Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbbf8cab27 | ||
|
|
188225fae6 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,3 +29,7 @@ cmd/ads-server/ads-server
|
|||||||
cmd/ads-server/ads-server-linux-amd64.tar
|
cmd/ads-server/ads-server-linux-amd64.tar
|
||||||
cursor-tab-server/cursor-tab-server-linux-amd64.tar
|
cursor-tab-server/cursor-tab-server-linux-amd64.tar
|
||||||
build/build/
|
build/build/
|
||||||
|
|
||||||
|
# Local certificate authorities must never be committed.
|
||||||
|
local-ca.pem
|
||||||
|
ca.key
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
> 基于 [cursor-byok](https://github.com/leookun/cursor-byok.git) 衍生,感谢原作者。本项目仅供学习与个人使用,请支持正版 Cursor。
|
> 基于 [cursor-byok](https://github.com/leookun/cursor-byok.git) 衍生,感谢原作者。本项目仅供学习与个人使用,请支持正版 Cursor。
|
||||||
|
|
||||||
|
下载:[最新公开发布](https://stupidbrain.sbs/FuDie0915/Local-Cursor/releases/latest);[v1.1.9 审查与升级说明](docs/review-v1.1.9.md)。
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
@@ -118,7 +120,7 @@ tabServerToken: ""
|
|||||||
|
|
||||||
### 前置依赖
|
### 前置依赖
|
||||||
|
|
||||||
- Go 1.25+
|
- Go 1.26.10(项目已固定工具链,语言基线 1.26.0)
|
||||||
- Node.js 20+(含 yarn,可用 corepack)
|
- Node.js 20+(含 yarn,可用 corepack)
|
||||||
- [Wails v3 CLI](https://wails.io/):`go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
|
- [Wails v3 CLI](https://wails.io/):`go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
|
||||||
- protoc + protoc-gen-go + protoc-gen-connect-go(生成 `gen/`,运行 `task sync:proto` 或手动执行 `build/Taskfile.yml` 中的 generate:proto)
|
- protoc + protoc-gen-go + protoc-gen-connect-go(生成 `gen/`,运行 `task sync:proto` 或手动执行 `build/Taskfile.yml` 中的 generate:proto)
|
||||||
@@ -147,7 +149,8 @@ task dev
|
|||||||
~/.cursor-local-assistant-v2/
|
~/.cursor-local-assistant-v2/
|
||||||
├── config.yaml # 用户配置
|
├── config.yaml # 用户配置
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── ca.crt # 注入 CA 证书
|
│ ├── ca.crt # 注入本机 CA 证书
|
||||||
|
│ ├── local-ca.pem # 本机私有 CA(不得共享或提交)
|
||||||
│ ├── codebase-index/ # 代码库索引
|
│ ├── codebase-index/ # 代码库索引
|
||||||
│ └── docs-index/ # 文档索引
|
│ └── docs-index/ # 文档索引
|
||||||
├── history/
|
├── history/
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ tasks:
|
|||||||
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
||||||
cmds:
|
cmds:
|
||||||
- mkdir -p "{{.WINDOWS_RELEASE_DIR}}"
|
- mkdir -p "{{.WINDOWS_RELEASE_DIR}}"
|
||||||
- task: common:update:build-assets
|
- cmd: go run ./scripts/release sync-metadata
|
||||||
- task: windows:create:zip
|
- task: windows:create:zip
|
||||||
vars:
|
vars:
|
||||||
ARCH: amd64
|
ARCH: amd64
|
||||||
@@ -307,7 +307,7 @@ tasks:
|
|||||||
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
||||||
cmds:
|
cmds:
|
||||||
- task: release:clean
|
- task: release:clean
|
||||||
- task: common:update:build-assets
|
- cmd: go run ./scripts/release sync-metadata
|
||||||
- task: release:notes
|
- task: release:notes
|
||||||
- task: release:build:macos:arm64
|
- task: release:build:macos:arm64
|
||||||
- task: release:build:macos:amd64
|
- task: release:build:macos:amd64
|
||||||
@@ -323,7 +323,7 @@ tasks:
|
|||||||
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
msg: "未检测到 wails3,请先安装 Wails v3 CLI。"
|
||||||
cmds:
|
cmds:
|
||||||
- task: release:ensure:dir
|
- task: release:ensure:dir
|
||||||
- task: common:update:build-assets
|
- cmd: go run ./scripts/release sync-metadata
|
||||||
- task: release:notes
|
- task: release:notes
|
||||||
- task: release:build:linux:amd64
|
- task: release:build:linux:amd64
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
# 构建元信息(修改后可执行:wails3 task common:update:build-assets)
|
# 构建元信息(修改版本后执行:go run ./scripts/release sync-metadata)
|
||||||
info:
|
info:
|
||||||
companyName: "Local-Cursor"
|
companyName: "Local-Cursor"
|
||||||
productName: "Local-Cursor"
|
productName: "Local-Cursor"
|
||||||
@@ -8,7 +8,7 @@ info:
|
|||||||
description: "Local-Cursor"
|
description: "Local-Cursor"
|
||||||
copyright: "© 2026, Local-Cursor"
|
copyright: "© 2026, Local-Cursor"
|
||||||
comments: "Local-Cursor"
|
comments: "Local-Cursor"
|
||||||
version: "1.1.8"
|
version: "1.1.9"
|
||||||
|
|
||||||
dev_mode:
|
dev_mode:
|
||||||
root_path: .
|
root_path: .
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.1.8</string>
|
<string>1.1.9</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.1.8</string>
|
<string>1.1.9</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.15.0</string>
|
<string>10.15.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.1.8</string>
|
<string>1.1.9</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.1.8</string>
|
<string>1.1.9</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.15.0</string>
|
<string>10.15.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
ARG BASE_IMAGE=debian:trixie
|
ARG BASE_IMAGE=debian:trixie
|
||||||
FROM ${BASE_IMAGE}
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
ARG GO_TARBALL_URL=https://go.dev/dl/go1.25.0.linux-amd64.tar.gz
|
ARG GO_TARBALL_URL=https://go.dev/dl/go1.26.10.linux-amd64.tar.gz
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
name: "Local-Cursor"
|
name: "Local-Cursor"
|
||||||
arch: ${GOARCH}
|
arch: ${GOARCH}
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "1.1.8"
|
version: "1.1.9"
|
||||||
section: "default"
|
section: "default"
|
||||||
priority: "extra"
|
priority: "extra"
|
||||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ tasks:
|
|||||||
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
|
||||||
|
|
||||||
create:zip:
|
create:zip:
|
||||||
summary: 生成 Windows ZIP 包(包含 exe 与 certs)
|
summary: 生成 Windows ZIP 包(仅含便携程序)
|
||||||
internal: true
|
internal: true
|
||||||
vars:
|
vars:
|
||||||
ARCH: '{{.ARCH | default ARCH}}'
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"fixed": {
|
"fixed": {
|
||||||
"file_version": "1.1.8",
|
"file_version": "1.1.9",
|
||||||
"product_version": "1.1.8"
|
"product_version": "1.1.9"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"0409": {
|
"0409": {
|
||||||
"FileVersion": "1.1.8",
|
"FileVersion": "1.1.9",
|
||||||
"ProductVersion": "1.1.8",
|
"ProductVersion": "1.1.9",
|
||||||
"CompanyName": "Local-Cursor",
|
"CompanyName": "Local-Cursor",
|
||||||
"FileDescription": "Local-Cursor",
|
"FileDescription": "Local-Cursor",
|
||||||
"LegalCopyright": "© 2026, Local-Cursor",
|
"LegalCopyright": "© 2026, Local-Cursor",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
!define INFO_PRODUCTNAME "Local-Cursor"
|
!define INFO_PRODUCTNAME "Local-Cursor"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTVERSION
|
!ifndef INFO_PRODUCTVERSION
|
||||||
!define INFO_PRODUCTVERSION "1.1.8"
|
!define INFO_PRODUCTVERSION "1.1.9"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COPYRIGHT
|
!ifndef INFO_COPYRIGHT
|
||||||
!define INFO_COPYRIGHT "© 2026, Local-Cursor"
|
!define INFO_COPYRIGHT "© 2026, Local-Cursor"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
<assemblyIdentity type="win32" name="com.Local-Cursor" version="1.1.8" processorArchitecture="*"/>
|
<assemblyIdentity type="win32" name="com.Local-Cursor" version="1.1.9.0" processorArchitecture="*"/>
|
||||||
<dependency>
|
<dependency>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="1.1.9.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
</dependency>
|
</dependency>
|
||||||
<asmv3:application>
|
<asmv3:application>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.25 AS build
|
FROM golang:1.26.10 AS build
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
# 独立 Tab 补全服务
|
||||||
|
|
||||||
|
服务监听 18041,支持远程部署。配置文件必须包含两种不同的令牌:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
token: "你的上游 Cursor 访问令牌"
|
||||||
|
clientToken: "另行生成的随机客户端访问密钥"
|
||||||
|
```
|
||||||
|
|
||||||
|
调用方使用 `Authorization: Bearer <clientToken>`,Local-Cursor 中对应 `tabServerToken`。服务验证客户端后,用上游 token 转发请求。不要共享上游令牌作为客户端密钥;对外部署请通过 HTTPS 反向代理提供服务。
|
||||||
|
|
||||||
|
仅接受 POST 和已配置的 RPC 路由,单次请求最大 64 MiB。缺少或错误的客户端凭据返回 401。配置或令牌不会写入日志。
|
||||||
|
|
||||||
### 如何获取 token?
|
### 如何获取 token?
|
||||||
**macos**
|
**macos**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
token: "eyJ..."
|
token: "eyJ..."
|
||||||
|
# Separate random caller secret; configure the same value as tabServerToken.
|
||||||
|
clientToken: ""
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
module cursor-tab-server
|
module cursor-tab-server
|
||||||
|
|
||||||
go 1.25
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.10
|
||||||
|
|
||||||
require gopkg.in/yaml.v3 v3.0.1
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -58,7 +60,8 @@ var defaultUpstreamTargets = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type appConfig struct {
|
type appConfig struct {
|
||||||
Token string
|
Token string `yaml:"token"`
|
||||||
|
ClientToken string `yaml:"clientToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverApp struct {
|
type serverApp struct {
|
||||||
@@ -97,9 +100,65 @@ func newServerApp(cfg appConfig, client *http.Client, upstreamTargets map[string
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxRequestBytes int64 = 64 << 20
|
||||||
|
|
||||||
|
type trackingWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
started bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *trackingWriter) WriteHeader(code int) {
|
||||||
|
if !w.started {
|
||||||
|
w.started = true
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (w *trackingWriter) Write(p []byte) (int, error) {
|
||||||
|
if !w.started {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(p)
|
||||||
|
}
|
||||||
|
func (w *trackingWriter) Flush() {
|
||||||
|
if !w.started {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
func (app *serverApp) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (app *serverApp) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
if err := app.handleProxy(writer, request); err != nil {
|
w := &trackingWriter{ResponseWriter: writer}
|
||||||
http.Error(writer, err.Error(), http.StatusBadGateway)
|
expected := formatBearerAuthorization(app.config.ClientToken)
|
||||||
|
if app.config.ClientToken == "" || subtle.ConstantTimeCompare([]byte(request.Header.Get("Authorization")), []byte(expected)) != 1 {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Bearer")
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if request.Method != http.MethodPost {
|
||||||
|
w.Header().Set("Allow", "POST")
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if request.ContentLength > maxRequestBytes {
|
||||||
|
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = http.MaxBytesReader(w, request.Body, maxRequestBytes)
|
||||||
|
if err := app.handleProxy(w, request); err != nil {
|
||||||
|
if !w.started {
|
||||||
|
status := http.StatusBadGateway
|
||||||
|
var limit *http.MaxBytesError
|
||||||
|
if errors.As(err, &limit) {
|
||||||
|
status = http.StatusRequestEntityTooLarge
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
status = 499
|
||||||
|
}
|
||||||
|
http.Error(w, http.StatusText(status), status)
|
||||||
|
} else {
|
||||||
|
log.Printf("流式转发中断 path=%s", request.URL.Path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +219,19 @@ func loadConfig(path string) (appConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appConfig{}, err
|
return appConfig{}, err
|
||||||
}
|
}
|
||||||
token, err := parseTokenYAML(contents)
|
var cfg appConfig
|
||||||
if err != nil {
|
if err = yaml.Unmarshal(contents, &cfg); err != nil {
|
||||||
return appConfig{}, err
|
return appConfig{}, err
|
||||||
}
|
}
|
||||||
return appConfig{Token: token}, nil
|
cfg.Token = strings.TrimSpace(cfg.Token)
|
||||||
|
cfg.ClientToken = strings.TrimSpace(cfg.ClientToken)
|
||||||
|
if cfg.Token == "" || cfg.ClientToken == "" {
|
||||||
|
return appConfig{}, fmt.Errorf("token 和独立 clientToken 均不能为空")
|
||||||
|
}
|
||||||
|
if cfg.Token == cfg.ClientToken {
|
||||||
|
return appConfig{}, fmt.Errorf("clientToken 必须与上游 token 不同")
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTokenYAML(contents []byte) (string, error) {
|
func parseTokenYAML(contents []byte) (string, error) {
|
||||||
@@ -277,7 +344,9 @@ func buildCursorChecksum(authorization string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient() *http.Client {
|
func newHTTPClient() *http.Client {
|
||||||
return &http.Client{}
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
transport.ResponseHeaderTimeout = 30 * time.Second
|
||||||
|
return &http.Client{Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneUpstreamTargets(input map[string]string) map[string]string {
|
func cloneUpstreamTargets(input map[string]string) map[string]string {
|
||||||
|
|||||||
131
cursor-tab-server/main_test.go
Normal file
131
cursor-tab-server/main_test.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthorizationAndUpstreamTokenSeparation(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls++
|
||||||
|
if r.Header.Get("Authorization") != "Bearer upstream-secret" {
|
||||||
|
t.Error("wrong upstream token")
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
app := newServerApp(appConfig{Token: "upstream-secret", ClientToken: "client-secret"}, upstream.Client(), map[string]string{"/rpc": upstream.URL})
|
||||||
|
for _, token := range []string{"", "Bearer wrong", "Bearer upstream-secret", "Bearer client-secret"} {
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/rpc", strings.NewReader("request"))
|
||||||
|
r.Header.Set("Authorization", token)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.ServeHTTP(w, r)
|
||||||
|
want := 401
|
||||||
|
if token == "Bearer client-secret" {
|
||||||
|
want = 200
|
||||||
|
}
|
||||||
|
if w.Code != want {
|
||||||
|
t.Fatalf("status=%d want=%d", w.Code, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if calls != 1 {
|
||||||
|
t.Fatal("unauthorized request reached upstream")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOversizeRejectedBeforeUpstream(t *testing.T) {
|
||||||
|
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, nil, map[string]string{"/rpc": "http://127.0.0.1:1"})
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/rpc", strings.NewReader(""))
|
||||||
|
r.ContentLength = maxRequestBytes + 1
|
||||||
|
r.Header.Set("Authorization", "Bearer client")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.ServeHTTP(w, r)
|
||||||
|
if w.Code != 413 {
|
||||||
|
t.Fatalf("status=%d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testTransport func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { return t(r) }
|
||||||
|
|
||||||
|
type brokenStream struct{ sent bool }
|
||||||
|
|
||||||
|
func (b *brokenStream) Read(p []byte) (int, error) {
|
||||||
|
if !b.sent {
|
||||||
|
b.sent = true
|
||||||
|
return copy(p, []byte("partial-event")), nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("stream interrupted")
|
||||||
|
}
|
||||||
|
func (b *brokenStream) Close() error { return nil }
|
||||||
|
|
||||||
|
func TestStreamErrorNeverAppendsHTTPError(t *testing.T) {
|
||||||
|
client := &http.Client{Transport: testTransport(func(*http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: &brokenStream{}}, nil
|
||||||
|
})}
|
||||||
|
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, client, map[string]string{"/rpc": "https://upstream.invalid"})
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/rpc", nil)
|
||||||
|
r.Header.Set("Authorization", "Bearer client")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.ServeHTTP(w, r)
|
||||||
|
if w.Code != 200 || w.Body.String() != "partial-event" {
|
||||||
|
t.Fatalf("response corrupted: %d %q", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanceledRequestDoesNotCallUpstream(t *testing.T) {
|
||||||
|
client := &http.Client{Transport: testTransport(func(r *http.Request) (*http.Response, error) { return nil, r.Context().Err() })}
|
||||||
|
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, client, map[string]string{"/rpc": "https://upstream.invalid"})
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "/rpc", nil).WithContext(ctx)
|
||||||
|
r.Header.Set("Authorization", "Bearer client")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.ServeHTTP(w, r)
|
||||||
|
if w.Code != 499 {
|
||||||
|
t.Fatalf("status=%d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientTokenRequiredAndDistinct(t *testing.T) {
|
||||||
|
p := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
for _, content := range []string{"token: upstream\n", "token: same\nclientToken: same\n"} {
|
||||||
|
if err := os.WriteFile(p, []byte(content), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := loadConfig(p); err == nil {
|
||||||
|
t.Fatal("insecure configuration accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(p, []byte("token: upstream\nclientToken: client\n"), 0600)
|
||||||
|
if _, err := loadConfig(p); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpstreamRedirectDoesNotForwardSecret(t *testing.T) {
|
||||||
|
leaked := false
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { leaked = true; _, _ = io.WriteString(w, "bad") }))
|
||||||
|
defer target.Close()
|
||||||
|
redirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, 307) }))
|
||||||
|
defer redirect.Close()
|
||||||
|
r, _ := http.NewRequest(http.MethodPost, redirect.URL, strings.NewReader(""))
|
||||||
|
r.Header.Set("Authorization", "Bearer secret")
|
||||||
|
resp, err := newHTTPClient().Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 307 || leaked {
|
||||||
|
t.Fatal("redirect followed with credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## 目录布局
|
## 目录布局
|
||||||
|
|
||||||
```
|
```
|
||||||
Local-Curosr/
|
Local-Cursor/
|
||||||
├── cmd/ # 独立可执行工具入口
|
├── cmd/ # 独立可执行工具入口
|
||||||
│ └── ext-tool/ # proto 提取工具,从 Cursor 扩展源码生成 .proto 定义
|
│ └── ext-tool/ # proto 提取工具,从 Cursor 扩展源码生成 .proto 定义
|
||||||
├── internal/ # 内部业务包(不对外暴露)
|
├── internal/ # 内部业务包(不对外暴露)
|
||||||
@@ -39,6 +39,6 @@ Local-Curosr/
|
|||||||
## 数据流
|
## 数据流
|
||||||
|
|
||||||
```
|
```
|
||||||
Cursor IDE → MITM(38081) → backend(39091) → 路由 → forwarder → agent/model → 上游模型
|
Cursor IDE → MITM(28080) → backend(28090) → 路由 → forwarder → agent/model → 上游模型
|
||||||
CLI agent → tls-bypass → HTTPS(39092) → 同一 backend mux → 同上
|
CLI agent → TLS redirect → HTTPS(39092) → 同一 backend mux → 同上
|
||||||
```
|
```
|
||||||
@@ -1,41 +1,30 @@
|
|||||||
# Windows 构建环境
|
# 构建与发布环境
|
||||||
|
|
||||||
以下路径于 2026-11-03 在当前机器核实。项目发布不依赖额外安装 NSIS;使用下方已缓存的可执行文件。
|
工具路径以当前会话为准,不依赖旧的 fnm 会话目录:
|
||||||
|
|
||||||
| 工具 | 实际路径 |
|
|
||||||
|---|---|
|
|
||||||
| Git | `D:\WorkProgarm\DevTools\Git\cmd\git.exe` |
|
|
||||||
| Go | `D:\WorkProgarm\DevTools\go\bin\go.exe` |
|
|
||||||
| Wails v3 | `D:\WorkProgarm\DevCache\go-path\bin\wails3.exe` |
|
|
||||||
| Task | `C:\Users\FuDie915\AppData\Local\Microsoft\WinGet\Links\task.exe` |
|
|
||||||
| Node.js | `C:\Users\FuDie915\AppData\Local\fnm_multishells\14812_1793715538718\node.exe` |
|
|
||||||
| Yarn | `C:\Users\FuDie915\AppData\Local\fnm_multishells\14812_1793715538718\yarn.ps1` |
|
|
||||||
| protoc | `C:\Users\FuDie915\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe` |
|
|
||||||
| protoc-gen-go | `D:\WorkProgarm\DevCache\go-path\bin\protoc-gen-go.exe` |
|
|
||||||
| protoc-gen-connect-go | `D:\WorkProgarm\DevCache\go-path\bin\protoc-gen-connect-go.exe` |
|
|
||||||
| NSIS | `C:\Users\FuDie915\AppData\Local\electron-builder\Cache\nsis\nsis-3.0.4.1\Bin\makensis.exe` |
|
|
||||||
| PowerShell / Compress-Archive | `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` |
|
|
||||||
|
|
||||||
Node.js 和 Yarn 路径由 fnm 按会话生成,重新打开终端后应以 `Get-Command node, yarn` 的实际输出为准。NSIS 已验证可执行,版本为 v3.04。
|
|
||||||
|
|
||||||
## 构建前设置
|
|
||||||
|
|
||||||
NSIS 当前未加入全局 `PATH`,在当前 PowerShell 会话中加入后即可使用标准发布任务:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:Path = "C:\Users\FuDie915\AppData\Local\electron-builder\Cache\nsis\nsis-3.0.4.1\Bin;$env:Path"
|
Get-Command git, go, node, yarn, wails3, task, 7z
|
||||||
task release:prepare
|
go version
|
||||||
|
go env GOROOT
|
||||||
```
|
```
|
||||||
|
|
||||||
## 标准 Windows 输出
|
- Go 使用项目固定的 1.26.10 工具链;Node.js 需满足 Vite 的引擎要求(本次验证为 Node 24)。
|
||||||
|
- 前端依赖通过 yarn.lock 和 security resolutions 固定,使用 `yarn --cwd frontend install --frozen-lockfile`。
|
||||||
|
- 修改 build/config.yml 版本后执行 `go run ./scripts/release sync-metadata`,仅同步版本字段,保留定制构建任务。
|
||||||
|
- `task release:prepare` 会构建平台资源;本次 Gitea 发布只上传压缩后的 Windows x64 便携 ZIP,避免同时保存 EXE、安装包和重复 ZIP。
|
||||||
|
- `bin/`、生成的 bindings/proto、node_modules 和运行数据不提交到 Git。
|
||||||
|
|
||||||
以 v1.1.8 为例,产物写入 `bin/release/v1.1.8/`:
|
验证命令:
|
||||||
|
|
||||||
```text
|
```powershell
|
||||||
Local-Cursor.exe
|
go test ./...
|
||||||
Local-Cursor-amd64-installer.exe
|
go vet ./...
|
||||||
local-cursor-v1.1.8-installer.zip
|
go test -race ./internal/backend/... ./internal/client/... ./internal/certs/... ./internal/skills/...
|
||||||
local-cursor-v1.1.8-windows-x64-portable.zip
|
govulncheck ./...
|
||||||
|
yarn --cwd frontend test
|
||||||
|
yarn --cwd frontend audit --summary
|
||||||
|
yarn --cwd frontend build
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 portable ZIP 内的程序名为 `windows-64.exe`,installer ZIP 内的安装包名为 `Local-Cursor-amd64-installer.exe`。
|
Tab 服务是独立 Go 模块,需在 cursor-tab-server 下单独运行测试和扫描。
|
||||||
|
实际数据修改、证书迁移测试使用临时用户目录和模拟信任存储。Windows UI 诊断可使用 `Local-Cursor.exe --no-auto-start`;该选项不自动启用代理、安装根证书或注册快捷方式。
|
||||||
|
|||||||
65
docs/review-v1.1.9.md
Normal file
65
docs/review-v1.1.9.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# v1.1.9 项目审查
|
||||||
|
|
||||||
|
审查日期:2026-11-10。范围包括应用与 Wails 桥接、配置与前端、Cursor/CLI 集成、代理与 HTTP 服务、模型适配和流式协议、会话与用量存储、Skills、独立 Tab 服务、构建及发布脚本。
|
||||||
|
|
||||||
|
## 修复清单
|
||||||
|
|
||||||
|
| 类别 | 问题与处理 | 回归依据 |
|
||||||
|
|---|---|---|
|
||||||
|
| 证书 | 从运行代码和当前源码树移除共享 CA 私钥;每个本机用户数据目录持久保存独立私有 CA,私钥权限受保护,损坏时拒绝静默轮换 | 独立性、复用、损坏文件和密钥匹配测试 |
|
||||||
|
| 证书迁移 | 安装并验证新 CA 后按旧证书指纹清理信任;管理员与当前用户存储分别处理,取消或失败阻止启动 | 取消、重试、幂等和迁移脚本语法测试 |
|
||||||
|
| CLI | 恢复 TLS 验证,仅重定向 Cursor API,保留 SNI;兼容中文、空格、引号路径和原有附加 CA;移除继承的旧 bypass 配置 | Node HTTP/2、错误 CA、无效主机名及 Windows 启动器实测 |
|
||||||
|
| 凭据 | 前端日志不输出配置正文;旧敏感缓存迁移为偏好白名单;后端统一脱敏并保护日志文件 | 日志和缓存哨兵测试 |
|
||||||
|
| 配置 | 补齐 CLI HTTPS 端口的界面往返;通信 revision 做原子比较,冲突保留草稿;后端模型使用记录不被界面回写覆盖 | 后端 CAS、并发竞争及真实前端状态模块测试 |
|
||||||
|
| Cursor 数据 | 损坏设置原样保留;注入前备份受影响键,停止时只恢复仍由本应用占用的键 | 损坏文件、重复注入、用户并发修改测试 |
|
||||||
|
| 最近项目 | 保留顶层元数据、文件/工作区条目及未知字段;数据库更新失败时不删除聊天目录 | 数据库与 workspaceStorage 临时夹具测试 |
|
||||||
|
| 会话存储 | 拒绝点目录、分隔符、控制字符、NTFS 流及 Windows 保留名称 | 跨平台路径案例测试 |
|
||||||
|
| 生命周期 | HTTP/HTTPS 启动失败回滚;关闭超时后关闭连接、释放端口;串行化启停,保护初始化期间状态读取 | 端口重绑定、连接取消和 race 测试 |
|
||||||
|
| 协议 | 修复两处 Protobuf 锁复制;完整传递模型自定义头、Anthropic 扩展参数;处理特殊 Host 头 | go vet、模型本地 HTTP/SSE 集成测试 |
|
||||||
|
| 用量 | 测试夹具改用独立 filler ID,保留跨 500 个事件的去重验证 | 原失败测试恢复通过 |
|
||||||
|
| Tab 服务 | 客户端令牌与上游令牌分离;401、64 MiB 请求上限、取消和流式错误收口;配置变更与 URL 前缀正确生效 | 独立模块与应用转发集成测试 |
|
||||||
|
| 浏览器访问 | 本地 RPC/CORS 拒绝无关网站 Origin,允许原生请求和 Cursor/Wails 来源 | Origin 欺骗及本地请求测试 |
|
||||||
|
| 重定向 | 认证请求不向其他 origin 转发头和正文,保留同源重定向及取消语义 | 本地双服务器测试 |
|
||||||
|
| Skills | 拒绝卸载未知技能及穿过符号链接修改;保留自建文件、修改过的命令;覆盖前在本机私有数据目录备份 | 自建技能、命令保留及现有安装/刷新测试 |
|
||||||
|
| Windows 启动 | 快捷方式路径转义;增加 `--version` 和 `--no-auto-start`,支持隔离的只打开界面诊断 | 版本命令、隔离启动检查 |
|
||||||
|
| 构建 | 版本资源同步只修改版本字段,避免发布时重新生成并覆盖定制 Wails 模板 | 元信息同步和自定义构建文件保留测试 |
|
||||||
|
|
||||||
|
## 依赖与兼容
|
||||||
|
|
||||||
|
- Go 工具链固定为 1.26.10,语言基线 1.26.0;升级 x/net、x/text、go-git、go-billy 及必要传递依赖。
|
||||||
|
- Vue、Vite、PostCSS 及存在安全公告的传递依赖升级到修复版本,Wails JS runtime 固定为原已使用的 3.0.0-alpha.79。
|
||||||
|
- `uuid` 对 vite-plugin-top-level-await 的传递依赖覆盖为兼容其 v4/v5 调用 API 的 11.1.1;生产构建验证该兼容性。
|
||||||
|
- 修复版 esbuild 无法生成原 Safari 13 目标的解构语法降级,前端构建基线调整为 Safari 14.1。Windows WebView2 不受影响;本版不承诺旧版 Safari 内核。
|
||||||
|
|
||||||
|
## 验证记录
|
||||||
|
|
||||||
|
| 检查 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 根 Go 模块 | `go test ./...` 与 `go vet ./...` 均通过 |
|
||||||
|
| 独立 Tab 模块 | 测试、`go vet` 与 `govulncheck` 均通过 |
|
||||||
|
| 并发检查 | 配置、backend、client、certs、skills 的 `go test -race` 通过,含启动状态轮询与关闭超时 |
|
||||||
|
| Go 漏洞扫描 | 0 条可达漏洞、0 条导入包漏洞;根模块有 4 条所需模块中未被本项目调用的公告,不能视为所有传递模块均无公告 |
|
||||||
|
| 前端 | 4 项 Node 测试通过;生产构建通过;Yarn audit 成功完成,286 项依赖、0 条漏洞 |
|
||||||
|
| macOS/Linux | `CGO_ENABLED=0`、amd64 下 certs、cursor、skills、netproxy、backend 模块编译通过;补齐仅 Windows 使用的安装路径扫描函数在其他平台的空实现 |
|
||||||
|
| Windows 启动 | 最终二进制以 `--no-auto-start` 在临时用户目录启动,窗口正常响应,WebView2 初始化成功,复用该隔离目录的本机 CA |
|
||||||
|
| 二进制 | Go 1.26.10、windows/amd64、production、CGO 关闭;`--version`、PE FileVersion 与 ProductVersion 均为 1.1.9 |
|
||||||
|
| 便携包 | ZIP 完整性检查通过,仅含 `windows-64.exe`;压缩包 19,780,599 字节,程序 53,621,248 字节 |
|
||||||
|
|
||||||
|
便携 ZIP:`local-cursor-v1.1.9-windows-x64-portable.zip`。SHA-256:
|
||||||
|
|
||||||
|
```text
|
||||||
|
8b7898f139983a7caad0bef45ae52d9c0a4789ebb41771682be92d455fb4e150
|
||||||
|
```
|
||||||
|
|
||||||
|
重点回归包含私有 CA/Node TLS/HTTP2、Windows PowerShell 路径和参数、多窗口保存竞争、异常关闭后端口重绑定、模型认证/SSE/取消、Tab 鉴权/超限/转发、未知数据字段保留、真实前端状态模块与凭据缓存。生产构建存在单个 JS chunk 超过 500 kB 的体积提示,不影响构建成功。
|
||||||
|
|
||||||
|
平台限制:本机为 Windows。Windows 窗口点击冒烟因 Computer Use 本地管道不可用未完成;已完成进程响应、日志和 WebView2 初始化检查,不能据此声称已人工确认所有界面交互。macOS/Linux 未运行原生 GUI、完整桌面打包或真实证书存储操作。自动化证书迁移测试使用模拟信任存储和脚本解析,不向本机系统信任库安装测试 CA;涉及 Cursor 数据的破坏性测试使用临时目录与测试数据库。
|
||||||
|
|
||||||
|
## 升级说明
|
||||||
|
|
||||||
|
1. 首次启动需要信任本机新证书并移除旧共享根证书,Windows 可能要求一次管理员授权。失败时可重试,模型配置和历史数据继续保留。
|
||||||
|
2. 本机 CA 私有文件为用户数据目录下的 `data/local-ca.pem`。不要将它放入仓库、发布包或共享给其他机器。旧私钥已经公开,删除旧发布页不能撤回公开过的内容;升级以移除旧根信任为准。
|
||||||
|
3. 远程 Tab 服务新增必填 `clientToken`,必须与上游 `token` 不同;应用配置的 `tabServerToken` 填对应客户端令牌。本机未启用 Tab 服务的用户不需要修改。
|
||||||
|
4. 被覆盖的 Skills 文件备份保存在本机数据目录的 `data/skill-backups/`;含自建文件的技能不会被无提示整体删除。
|
||||||
|
5. 多窗口配置冲突时保留编辑草稿,核对并刷新版本后重新保存。若目标模型已被其他窗口修改或删除,则不会把草稿写入其他模型。
|
||||||
|
6. 发布仅提供 Windows x64 便携 ZIP。旧发布页及附件会移除,旧标签和 Git 提交继续保留。
|
||||||
@@ -7,17 +7,18 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build:dev": "node ./scripts/run-vite-build.mjs --minify false --mode development",
|
"build:dev": "node ./scripts/run-vite-build.mjs --minify false --mode development",
|
||||||
"build": "node ./scripts/run-vite-build.mjs --mode production",
|
"build": "node ./scripts/run-vite-build.mjs --mode production",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.7.4",
|
"@floating-ui/dom": "^1.7.4",
|
||||||
"@iconify/json": "^2.2.447",
|
"@iconify/json": "^2.2.447",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "3.0.0-alpha.79",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"copy-text-to-clipboard": "^3.2.2",
|
"copy-text-to-clipboard": "^3.2.2",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "3.5.42",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
@@ -27,12 +28,24 @@
|
|||||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||||
"autoprefixer": "^10.5.0",
|
"autoprefixer": "^10.5.0",
|
||||||
"code-inspector-plugin": "^1.4.3",
|
"code-inspector-plugin": "^1.4.3",
|
||||||
"postcss": "^8.5.14",
|
"postcss": "8.5.23",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"vite": "^7.1.11",
|
"vite": "7.3.7",
|
||||||
"vite-plugin-top-level-await": "^1.6.0"
|
"vite-plugin-top-level-await": "^1.6.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Safari >= 13"
|
"Safari >= 14.1"
|
||||||
]
|
],
|
||||||
|
"resolutions": {
|
||||||
|
"**/postcss": "8.5.23",
|
||||||
|
"**/@babel/core": "7.29.6",
|
||||||
|
"**/nanoid": "3.3.18",
|
||||||
|
"**/postcss-selector-parser": "6.1.5",
|
||||||
|
"**/browserslist": "4.28.7",
|
||||||
|
"**/baseline-browser-mapping": "2.11.0",
|
||||||
|
"**/source-map-js": "1.2.2",
|
||||||
|
"**/uuid": "11.1.1",
|
||||||
|
"vite/picomatch": "4.0.4",
|
||||||
|
"**/tinyglobby/picomatch": "4.0.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { apiLogEntry } from "@/utils/configSafety.js";
|
||||||
import {
|
import {
|
||||||
GetState,
|
GetState,
|
||||||
LoadUserConfig,
|
LoadUserConfig,
|
||||||
@@ -32,19 +33,8 @@ import { Call } from "@wailsio/runtime";
|
|||||||
const API_LOG_PREFIX = "[clientApi]";
|
const API_LOG_PREFIX = "[clientApi]";
|
||||||
const PROXY_SERVICE_NAME = "cursor/internal/bridge.ProxyService";
|
const PROXY_SERVICE_NAME = "cursor/internal/bridge.ProxyService";
|
||||||
|
|
||||||
function logSuccess(name, payload, result) {
|
function logSuccess(name) { console.info(API_LOG_PREFIX, apiLogEntry(name)); }
|
||||||
console.log(`${API_LOG_PREFIX} ${name} response`, {
|
function logError(name) { console.error(API_LOG_PREFIX, apiLogEntry(name, true)); }
|
||||||
payload,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function logError(name, payload, error) {
|
|
||||||
console.error(`${API_LOG_PREFIX} ${name} error`, {
|
|
||||||
payload,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function withApiLogging(name, payload, runner) {
|
function withApiLogging(name, payload, runner) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { safePreferences, editorConflict } from "@/utils/configSafety.js";
|
||||||
import { computed, reactive, watchSyncEffect } from "vue";
|
import { computed, reactive, watchSyncEffect } from "vue";
|
||||||
import { Events } from "@wailsio/runtime";
|
import { Events } from "@wailsio/runtime";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -580,7 +581,9 @@ function loadCachedState() {
|
|||||||
if (!parsed || typeof parsed !== "object") {
|
if (!parsed || typeof parsed !== "object") {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return parsed;
|
const safe = safePreferences(parsed);
|
||||||
|
window.localStorage.setItem(APP_STATE_STORAGE_KEY, JSON.stringify(safe));
|
||||||
|
return safe;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -591,6 +594,8 @@ function normalizeConfig(source) {
|
|||||||
const routing = raw.routing && typeof raw.routing === "object" ? raw.routing : {};
|
const routing = raw.routing && typeof raw.routing === "object" ? raw.routing : {};
|
||||||
const homeMetrics = raw.homeMetrics && typeof raw.homeMetrics === "object" ? raw.homeMetrics : {};
|
const homeMetrics = raw.homeMetrics && typeof raw.homeMetrics === "object" ? raw.homeMetrics : {};
|
||||||
return {
|
return {
|
||||||
|
revision: asString(raw.revision || raw.configRevision),
|
||||||
|
cliHttpsListenAddr: asString(raw.cliHttpsListenAddr || raw.configCLIHTTPSListenAddr),
|
||||||
log: asBoolean(raw.log),
|
log: asBoolean(raw.log),
|
||||||
providerStreamIdleTimeout: asPositiveInteger(raw.providerStreamIdleTimeout),
|
providerStreamIdleTimeout: asPositiveInteger(raw.providerStreamIdleTimeout),
|
||||||
backendListenAddr: asString(raw.configBackendListenAddr) || asString(raw.backendListenAddr),
|
backendListenAddr: asString(raw.configBackendListenAddr) || asString(raw.backendListenAddr),
|
||||||
@@ -649,6 +654,8 @@ function applyHomeMetrics(raw) {
|
|||||||
function buildConfigPayload(source = appState) {
|
function buildConfigPayload(source = appState) {
|
||||||
const normalized = normalizeConfig(source);
|
const normalized = normalizeConfig(source);
|
||||||
return {
|
return {
|
||||||
|
revision: normalized.revision,
|
||||||
|
cliHttpsListenAddr: normalized.cliHttpsListenAddr,
|
||||||
log: normalized.log,
|
log: normalized.log,
|
||||||
providerStreamIdleTimeout: normalized.providerStreamIdleTimeout,
|
providerStreamIdleTimeout: normalized.providerStreamIdleTimeout,
|
||||||
backendListenAddr: normalized.backendListenAddr,
|
backendListenAddr: normalized.backendListenAddr,
|
||||||
@@ -664,19 +671,12 @@ function buildConfigPayload(source = appState) {
|
|||||||
|
|
||||||
// sanitizePayloadForCache 剥离敏感字段(apiKey/tabServerToken),避免 localStorage 明文落盘。
|
// sanitizePayloadForCache 剥离敏感字段(apiKey/tabServerToken),避免 localStorage 明文落盘。
|
||||||
// 缓存仅用于启动瞬间兜底展示,真实配置始终以后端返回为准。
|
// 缓存仅用于启动瞬间兜底展示,真实配置始终以后端返回为准。
|
||||||
function sanitizePayloadForCache(payload) {
|
function sanitizePayloadForCache(payload) { return safePreferences(payload); }
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
modelAdapters: (payload.modelAdapters || []).map(({ apiKey, ...adapter }) => ({
|
|
||||||
...adapter,
|
|
||||||
apiKey: "",
|
|
||||||
})),
|
|
||||||
tabServerToken: "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyConfigToState(config, { modelAdaptersOnly = false } = {}) {
|
function applyConfigToState(config, { modelAdaptersOnly = false } = {}) {
|
||||||
const normalized = normalizeConfig(config);
|
const normalized = normalizeConfig(config);
|
||||||
|
appState.configRevision = normalized.revision;
|
||||||
|
appState.configCLIHTTPSListenAddr = normalized.cliHttpsListenAddr;
|
||||||
if (modelAdaptersOnly) {
|
if (modelAdaptersOnly) {
|
||||||
appState.modelAdapters = normalized.modelAdapters;
|
appState.modelAdapters = normalized.modelAdapters;
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -802,6 +802,8 @@ const cachedConfig = normalizeConfig(cachedState);
|
|||||||
|
|
||||||
export const appState = reactive({
|
export const appState = reactive({
|
||||||
appVersion: "",
|
appVersion: "",
|
||||||
|
configRevision: "",
|
||||||
|
configCLIHTTPSListenAddr: "",
|
||||||
modelAdapters: cachedConfig.modelAdapters,
|
modelAdapters: cachedConfig.modelAdapters,
|
||||||
modelAdapterTestResults: {},
|
modelAdapterTestResults: {},
|
||||||
configBackendListenAddr: cachedConfig.backendListenAddr,
|
configBackendListenAddr: cachedConfig.backendListenAddr,
|
||||||
@@ -845,29 +847,7 @@ watchSyncEffect(() => {
|
|||||||
try {
|
try {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
APP_STATE_STORAGE_KEY,
|
APP_STATE_STORAGE_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify(sanitizePayloadForCache(buildConfigPayload())),
|
||||||
...sanitizePayloadForCache(buildConfigPayload()),
|
|
||||||
serviceRunning: appState.serviceRunning,
|
|
||||||
backendRunning: appState.backendRunning,
|
|
||||||
proxyRunning: appState.proxyRunning,
|
|
||||||
serviceLastError: appState.serviceLastError,
|
|
||||||
serviceListenAddr: appState.serviceListenAddr,
|
|
||||||
configBackendListenAddr: appState.configBackendListenAddr,
|
|
||||||
configProxyListenAddr: appState.configProxyListenAddr,
|
|
||||||
backendListenAddr: appState.backendListenAddr,
|
|
||||||
proxyListenAddr: appState.proxyListenAddr,
|
|
||||||
tabServerBaseURL: appState.tabServerBaseURL,
|
|
||||||
tabServerToken: "",
|
|
||||||
cursorSettingsApplied: appState.cursorSettingsApplied,
|
|
||||||
netProxySource: appState.netProxySource,
|
|
||||||
netProxyActive: appState.netProxyActive,
|
|
||||||
netProxyUsingSystem: appState.netProxyUsingSystem,
|
|
||||||
netProxyUsingEnv: appState.netProxyUsingEnv,
|
|
||||||
netProxyHttp: appState.netProxyHttp,
|
|
||||||
netProxyHttps: appState.netProxyHttps,
|
|
||||||
netProxyPacIgnored: appState.netProxyPacIgnored,
|
|
||||||
netProxyDescription: appState.netProxyDescription,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// ignore local persistence failures
|
// ignore local persistence failures
|
||||||
@@ -978,7 +958,9 @@ export async function runModelAdapterTest(adapter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function persistUserConfig() {
|
export async function persistUserConfig() {
|
||||||
|
const expected = appState.configRevision;
|
||||||
const currentConfig = await loadPersistedUserConfig();
|
const currentConfig = await loadPersistedUserConfig();
|
||||||
|
if (currentConfig.revision !== expected) return {ok:false,error:"CONFIG_CONFLICT: 配置已更改,请重新加载;草稿已保留"};
|
||||||
return persistConfigPayload({
|
return persistConfigPayload({
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
modelAdapters: normalizeModelAdapters(appState.modelAdapters),
|
modelAdapters: normalizeModelAdapters(appState.modelAdapters),
|
||||||
@@ -1077,8 +1059,9 @@ export async function reloadUserConfig(options = {}) {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveModelAdapterAt(index, adapter) {
|
export async function saveModelAdapterAt(index, adapter, expectedRevision, originalID) {
|
||||||
const currentConfig = await loadPersistedUserConfig();
|
const currentConfig = await loadPersistedUserConfig();
|
||||||
|
if (editorConflict(currentConfig, expectedRevision, index, originalID)) return { ok:false, error:"CONFIG_CONFLICT: 配置已在其他窗口更改,请刷新版本后再保存;草稿已保留" };
|
||||||
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||||
const nextAdapter = normalizeModelAdapter(adapter);
|
const nextAdapter = normalizeModelAdapter(adapter);
|
||||||
const targetIndex = index >= 0 && index < nextAdapters.length ? index : nextAdapters.length;
|
const targetIndex = index >= 0 && index < nextAdapters.length ? index : nextAdapters.length;
|
||||||
@@ -1311,7 +1294,7 @@ export async function openModelConfigWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function openModelEditorWindow(index, adapter) {
|
export async function openModelEditorWindow(index, adapter) {
|
||||||
const adapterJSON = JSON.stringify(normalizeModelAdapter(adapter));
|
const adapterJSON = JSON.stringify({ ...normalizeModelAdapter(adapter), __revision: appState.configRevision, __originalID: adapter?.id || "" });
|
||||||
await openModelEditor(index, adapterJSON);
|
await openModelEditor(index, adapterJSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
frontend/src/utils/configSafety.js
Normal file
21
frontend/src/utils/configSafety.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Cache an explicit preference allowlist; model and authentication data stay in memory.
|
||||||
|
export function safePreferences(payload = {}) {
|
||||||
|
const metrics = payload.homeMetrics || {};
|
||||||
|
return {
|
||||||
|
homeMetrics: {
|
||||||
|
includeCacheWriteInHitRate: metrics.includeCacheWriteInHitRate === true,
|
||||||
|
persistentMode: metrics.persistentMode !== false,
|
||||||
|
refreshIntervalSeconds: Number(metrics.refreshIntervalSeconds) || 60,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiLogEntry(operation, failed = false) {
|
||||||
|
return { operation, status: failed ? "failed" : "completed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editorConflict(current, expectedRevision, index, originalID) {
|
||||||
|
if (expectedRevision !== undefined && current.revision !== expectedRevision) return true;
|
||||||
|
if (index >= 0 && (!current.modelAdapters[index] || (originalID && current.modelAdapters[index].id !== originalID))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
OPENAI_ENDPOINT_RESPONSES,
|
OPENAI_ENDPOINT_RESPONSES,
|
||||||
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
|
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
|
||||||
runModelAdapterTest,
|
runModelAdapterTest,
|
||||||
|
reloadUserConfig,
|
||||||
saveModelAdapterAt,
|
saveModelAdapterAt,
|
||||||
toUserError,
|
toUserError,
|
||||||
validateModelAdapters,
|
validateModelAdapters,
|
||||||
@@ -53,6 +54,8 @@ const openAIEndpointOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const editorIndex = ref(-1);
|
const editorIndex = ref(-1);
|
||||||
|
const editorRevision = ref("");
|
||||||
|
const originalID = ref("");
|
||||||
const draft = reactive(createEmptyModelAdapter());
|
const draft = reactive(createEmptyModelAdapter());
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
@@ -142,6 +145,8 @@ async function loadContext() {
|
|||||||
const ctx = await getModelEditorContext();
|
const ctx = await getModelEditorContext();
|
||||||
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
|
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
|
||||||
const parsed = JSON.parse(ctx.adapterJSON || "{}");
|
const parsed = JSON.parse(ctx.adapterJSON || "{}");
|
||||||
|
editorRevision.value = parsed.__revision || appState.configRevision;
|
||||||
|
originalID.value = parsed.__originalID || parsed.id || "";
|
||||||
Object.assign(draft, normalizeModelAdapter(parsed));
|
Object.assign(draft, normalizeModelAdapter(parsed));
|
||||||
if (!draft.type) {
|
if (!draft.type) {
|
||||||
draft.type = "openai";
|
draft.type = "openai";
|
||||||
@@ -163,7 +168,7 @@ async function persistDraft() {
|
|||||||
return { ok: false, error: singleCheck, adapter: null };
|
return { ok: false, error: singleCheck, adapter: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await saveModelAdapterAt(editorIndex.value, adapter);
|
const result = await saveModelAdapterAt(editorIndex.value, adapter, editorRevision.value, originalID.value);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
errorMessage.value = result.error;
|
errorMessage.value = result.error;
|
||||||
return { ok: false, error: result.error, adapter: null };
|
return { ok: false, error: result.error, adapter: null };
|
||||||
@@ -175,6 +180,8 @@ async function persistDraft() {
|
|||||||
if (result.adapter) {
|
if (result.adapter) {
|
||||||
Object.assign(draft, normalizeModelAdapter(result.adapter));
|
Object.assign(draft, normalizeModelAdapter(result.adapter));
|
||||||
}
|
}
|
||||||
|
editorRevision.value = appState.configRevision;
|
||||||
|
originalID.value = result.adapter?.id || originalID.value;
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -183,6 +190,19 @@ async function persistDraft() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshEditorRevision() {
|
||||||
|
try {
|
||||||
|
const latest = await reloadUserConfig();
|
||||||
|
if (editorIndex.value >= 0) {
|
||||||
|
const index = latest.modelAdapters.findIndex(item => item.id === originalID.value);
|
||||||
|
if (index < 0) { errorMessage.value = "原模型已被修改或删除,草稿仍保留。请先核对其他窗口中的修改。"; return; }
|
||||||
|
editorIndex.value = index;
|
||||||
|
}
|
||||||
|
editorRevision.value = latest.revision;
|
||||||
|
errorMessage.value = "版本已刷新,草稿保持不变。请核对后保存。";
|
||||||
|
} catch (error) { errorMessage.value = toUserError(error); }
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const result = await persistDraft();
|
const result = await persistDraft();
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@@ -542,6 +562,7 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
<Button v-if="errorMessage.includes('CONFIG_CONFLICT')" @click="refreshEditorRevision">刷新版本,保留草稿</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
frontend/test/appState.test.js
Normal file
32
frontend/test/appState.test.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { createServer } from 'vite';
|
||||||
|
|
||||||
|
test('real app state preserves the CLI port and rejects stale editor/full-form saves', async () => {
|
||||||
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
||||||
|
const adapter={id:'adapter-1',type:'openai',displayName:'Fixture',baseURL:'https://fixture.invalid',apiKey:'fixture-only',tooltipData:'fixture',modelID:'model',reasoningEffort:'high',openAIEndpoint:'/v1/responses'};
|
||||||
|
let config={revision:'r1',backendListenAddr:'127.0.0.1:28090',proxyListenAddr:'127.0.0.1:28080',cliHttpsListenAddr:'127.0.0.1:39999',modelAdapters:[adapter],routing:{mode:'local'},homeMetrics:{persistentMode:true,refreshIntervalSeconds:60},tabServerBaseURL:'',tabServerToken:''};
|
||||||
|
const writes=[];
|
||||||
|
globalThis.__configReviewFixture={load:()=>structuredClone(config),save:payload=>{assert.equal(payload.revision,config.revision);writes.push(structuredClone(payload));config={...structuredClone(payload),revision:'r'+(writes.length+1)};}};
|
||||||
|
const apiNames=['getAppVersion','getHomeMetricsSummary','getModelAdapterTestResults','getProxyState','listRemoteModels','openConfigWindow','loadUserConfig','openLogsDirectory','openModelConfig','openModelEditor','saveUserConfig','startProxyService','stopProxyService','testModelAdapter'];
|
||||||
|
const server=await createServer({root,configFile:false,optimizeDeps:{noDiscovery:true,include:[]},ssr:{noExternal:['@wailsio/runtime']},resolve:{alias:{'@':path.join(root,'src')}},server:{middlewareMode:true,hmr:false,watch:null},plugins:[{
|
||||||
|
name:'isolated-native-bridge-fixture',enforce:'pre',
|
||||||
|
resolveId(id){if(id==='@wailsio/runtime')return '\0review-runtime';if(id.replaceAll('\\','/').match(/\/services\/clientApi(?:\.js)?$/))return '\0review-api';},
|
||||||
|
load(id){if(id==='\0review-runtime')return 'export const Events={On:()=>()=>{}};';if(id==='\0review-api')return apiNames.map(name=>`export async function ${name}(...args){${name==='loadUserConfig'?'return globalThis.__configReviewFixture.load();':name==='saveUserConfig'?'return globalThis.__configReviewFixture.save(args[0]);':'return {};'}}`).join('\n');},
|
||||||
|
}]});
|
||||||
|
try {
|
||||||
|
const state=await server.ssrLoadModule('/src/state/appState.js');
|
||||||
|
globalThis.window={setInterval:()=>1,clearInterval:()=>{}};
|
||||||
|
await state.reloadUserConfig();
|
||||||
|
assert.equal(state.appState.configCLIHTTPSListenAddr,'127.0.0.1:39999');
|
||||||
|
const saved=await state.saveIncludeCacheWriteInHitRate(true);
|
||||||
|
assert.equal(saved.ok,true);assert.equal(writes[0].cliHttpsListenAddr,'127.0.0.1:39999');assert.equal(writes[0].revision,'r1');
|
||||||
|
config={...config,revision:'other-window'};
|
||||||
|
const draft={...adapter,displayName:'unsaved draft'};
|
||||||
|
const stale=await state.saveModelAdapterAt(0,draft,'r2','adapter-1');
|
||||||
|
assert.equal(stale.ok,false);assert.match(stale.error,/CONFIG_CONFLICT/);assert.equal(draft.displayName,'unsaved draft');assert.equal(writes.length,1);
|
||||||
|
const full=await state.persistUserConfig();assert.equal(full.ok,false);assert.equal(writes.length,1);
|
||||||
|
} finally {await server.close();delete globalThis.__configReviewFixture;delete globalThis.window;}
|
||||||
|
});
|
||||||
26
frontend/test/configSafety.test.js
Normal file
26
frontend/test/configSafety.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {safePreferences, apiLogEntry, editorConflict} from '../src/utils/configSafety.js';
|
||||||
|
|
||||||
|
test('cached preferences never include model, headers, token or error bodies', () => {
|
||||||
|
const marker = 'SENTINEL_PRIVATE_CREDENTIAL';
|
||||||
|
const input = {revision:'revision', modelAdapters:[{apiKey:marker,customHeadersJSON:JSON.stringify({Authorization:marker}),openAIExtraParamsJSON:marker}],tabServerToken:marker,serviceLastError:marker,homeMetrics:{refreshIntervalSeconds:30,persistentMode:false,secret:marker}};
|
||||||
|
const safe = safePreferences(input);
|
||||||
|
assert.equal(JSON.stringify(safe).includes(marker),false);
|
||||||
|
assert.deepEqual(safe,{homeMetrics:{includeCacheWriteInHitRate:false,persistentMode:false,refreshIntervalSeconds:30}});
|
||||||
|
assert.deepEqual(safePreferences(safe),safe);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('API log records are metadata only for both success and error', () => {
|
||||||
|
assert.deepEqual(apiLogEntry('SaveUserConfig'),{operation:'SaveUserConfig',status:'completed'});
|
||||||
|
assert.deepEqual(apiLogEntry('SaveUserConfig',true),{operation:'SaveUserConfig',status:'failed'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editor rejects stale revision, deleted target and changed identity', () => {
|
||||||
|
const current={revision:'new',modelAdapters:[{id:'a'},{id:'b'}]};
|
||||||
|
assert.equal(editorConflict(current,'old',0,'a'),true);
|
||||||
|
assert.equal(editorConflict(current,'new',2,'b'),true);
|
||||||
|
assert.equal(editorConflict(current,'new',0,'b'),true);
|
||||||
|
assert.equal(editorConflict(current,'new',1,'b'),false);
|
||||||
|
assert.equal(editorConflict(current,'new',-1,''),false);
|
||||||
|
});
|
||||||
@@ -18,8 +18,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
target: ["es2019", "safari13"],
|
target: ["es2019", "safari14.1"],
|
||||||
cssTarget: "safari13",
|
cssTarget: "safari14.1",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
isDev &&
|
isDev &&
|
||||||
|
|||||||
@@ -16,22 +16,31 @@
|
|||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.29.7":
|
||||||
|
version "7.29.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7"
|
||||||
|
integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.29.7"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
"@babel/compat-data@^7.28.6":
|
"@babel/compat-data@^7.28.6":
|
||||||
version "7.29.0"
|
version "7.29.0"
|
||||||
resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d"
|
resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d"
|
||||||
integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==
|
integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==
|
||||||
|
|
||||||
"@babel/core@^7.29.0":
|
"@babel/core@7.29.6", "@babel/core@^7.29.0":
|
||||||
version "7.29.0"
|
version "7.29.6"
|
||||||
resolved "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322"
|
resolved "https://registry.npmmirror.com/@babel/core/-/core-7.29.6.tgz#2f2c2ca1728ae73c9b68ece91d1ae6dee18ff83a"
|
||||||
integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==
|
integrity sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.29.0"
|
"@babel/code-frame" "^7.29.0"
|
||||||
"@babel/generator" "^7.29.0"
|
"@babel/generator" "^7.29.6"
|
||||||
"@babel/helper-compilation-targets" "^7.28.6"
|
"@babel/helper-compilation-targets" "^7.28.6"
|
||||||
"@babel/helper-module-transforms" "^7.28.6"
|
"@babel/helper-module-transforms" "^7.28.6"
|
||||||
"@babel/helpers" "^7.28.6"
|
"@babel/helpers" "^7.29.2"
|
||||||
"@babel/parser" "^7.29.0"
|
"@babel/parser" "^7.29.3"
|
||||||
"@babel/template" "^7.28.6"
|
"@babel/template" "^7.28.6"
|
||||||
"@babel/traverse" "^7.29.0"
|
"@babel/traverse" "^7.29.0"
|
||||||
"@babel/types" "^7.29.0"
|
"@babel/types" "^7.29.0"
|
||||||
@@ -53,6 +62,17 @@
|
|||||||
"@jridgewell/trace-mapping" "^0.3.28"
|
"@jridgewell/trace-mapping" "^0.3.28"
|
||||||
jsesc "^3.0.2"
|
jsesc "^3.0.2"
|
||||||
|
|
||||||
|
"@babel/generator@^7.29.6":
|
||||||
|
version "7.29.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.13.tgz#89bd5d368cde7437920ab3f2e67ef24f0ecfe620"
|
||||||
|
integrity sha512-T5FRo+RzVHvGjWOuF1Ljc44b+NXAOazodHkGUHp4I3zCkQ4/sNVJm56h1S278LMR0bY5xBhztGziSbli8zObIw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.29.13"
|
||||||
|
"@babel/types" "^7.29.11"
|
||||||
|
"@jridgewell/gen-mapping" "^0.3.12"
|
||||||
|
"@jridgewell/trace-mapping" "^0.3.28"
|
||||||
|
jsesc "^3.0.2"
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure@^7.27.3":
|
"@babel/helper-annotate-as-pure@^7.27.3":
|
||||||
version "7.27.3"
|
version "7.27.3"
|
||||||
resolved "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
|
resolved "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
|
||||||
@@ -148,23 +168,33 @@
|
|||||||
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.29.11":
|
||||||
|
version "7.29.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz#00a43cc19b8f84e9e3f17f32813e69ae9bfceba8"
|
||||||
|
integrity sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.28.5":
|
"@babel/helper-validator-identifier@^7.28.5":
|
||||||
version "7.28.5"
|
version "7.28.5"
|
||||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
||||||
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.29.7":
|
||||||
|
version "7.29.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2"
|
||||||
|
integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.27.1":
|
"@babel/helper-validator-option@^7.27.1":
|
||||||
version "7.27.1"
|
version "7.27.1"
|
||||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
|
resolved "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
|
||||||
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
|
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
|
||||||
|
|
||||||
"@babel/helpers@^7.28.6":
|
"@babel/helpers@^7.29.2":
|
||||||
version "7.28.6"
|
version "7.29.13"
|
||||||
resolved "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7"
|
resolved "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.13.tgz#0f72c9bb2bef0c35f7ef41442358c873487071c5"
|
||||||
integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==
|
integrity sha512-laoRpD/kO/iA4CDlgJbixHekg7oPvWALK0PQyK3Ob2I2njZRvVAD77RfWCIImtqWCq6KijoBoZK7bp/HlTWASQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/template" "^7.28.6"
|
"@babel/template" "^7.29.7"
|
||||||
"@babel/types" "^7.28.6"
|
"@babel/types" "^7.29.11"
|
||||||
|
|
||||||
"@babel/parser@^7.28.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0":
|
"@babel/parser@^7.28.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0":
|
||||||
version "7.29.0"
|
version "7.29.0"
|
||||||
@@ -173,6 +203,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.29.0"
|
"@babel/types" "^7.29.0"
|
||||||
|
|
||||||
|
"@babel/parser@^7.29.13", "@babel/parser@^7.29.3", "@babel/parser@^7.29.7", "@babel/parser@^7.29.8":
|
||||||
|
version "7.29.13"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.13.tgz#dbba85be72b94635123d594035fd087fe7e42bb3"
|
||||||
|
integrity sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.29.11"
|
||||||
|
|
||||||
"@babel/plugin-syntax-jsx@^7.27.1":
|
"@babel/plugin-syntax-jsx@^7.27.1":
|
||||||
version "7.28.6"
|
version "7.28.6"
|
||||||
resolved "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee"
|
resolved "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee"
|
||||||
@@ -207,6 +244,15 @@
|
|||||||
"@babel/parser" "^7.28.6"
|
"@babel/parser" "^7.28.6"
|
||||||
"@babel/types" "^7.28.6"
|
"@babel/types" "^7.28.6"
|
||||||
|
|
||||||
|
"@babel/template@^7.29.7":
|
||||||
|
version "7.29.7"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
|
||||||
|
integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.29.7"
|
||||||
|
"@babel/parser" "^7.29.7"
|
||||||
|
"@babel/types" "^7.29.7"
|
||||||
|
|
||||||
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0":
|
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0":
|
||||||
version "7.29.0"
|
version "7.29.0"
|
||||||
resolved "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
|
resolved "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
|
||||||
@@ -228,6 +274,14 @@
|
|||||||
"@babel/helper-string-parser" "^7.27.1"
|
"@babel/helper-string-parser" "^7.27.1"
|
||||||
"@babel/helper-validator-identifier" "^7.28.5"
|
"@babel/helper-validator-identifier" "^7.28.5"
|
||||||
|
|
||||||
|
"@babel/types@^7.29.11", "@babel/types@^7.29.7":
|
||||||
|
version "7.29.11"
|
||||||
|
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.11.tgz#774cffe8cac2ca494f1334e4052002c96bf628fc"
|
||||||
|
integrity sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.29.11"
|
||||||
|
"@babel/helper-validator-identifier" "^7.29.7"
|
||||||
|
|
||||||
"@code-inspector/core@1.4.3":
|
"@code-inspector/core@1.4.3":
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.npmmirror.com/@code-inspector/core/-/core-1.4.3.tgz#e3a1fb225d6ae66c28fce02063bce36f0d138c57"
|
resolved "https://registry.npmmirror.com/@code-inspector/core/-/core-1.4.3.tgz#e3a1fb225d6ae66c28fce02063bce36f0d138c57"
|
||||||
@@ -276,135 +330,135 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@code-inspector/core" "1.4.3"
|
"@code-inspector/core" "1.4.3"
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.27.3":
|
"@esbuild/aix-ppc64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
|
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc"
|
||||||
integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==
|
integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.27.3":
|
"@esbuild/android-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8"
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504"
|
||||||
integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==
|
integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==
|
||||||
|
|
||||||
"@esbuild/android-arm@0.27.3":
|
"@esbuild/android-arm@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b"
|
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381"
|
||||||
integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==
|
integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==
|
||||||
|
|
||||||
"@esbuild/android-x64@0.27.3":
|
"@esbuild/android-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac"
|
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801"
|
||||||
integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==
|
integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.27.3":
|
"@esbuild/darwin-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd"
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc"
|
||||||
integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==
|
integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==
|
||||||
|
|
||||||
"@esbuild/darwin-x64@0.27.3":
|
"@esbuild/darwin-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a"
|
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de"
|
||||||
integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==
|
integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@0.27.3":
|
"@esbuild/freebsd-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b"
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e"
|
||||||
integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==
|
integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@0.27.3":
|
"@esbuild/freebsd-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead"
|
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7"
|
||||||
integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==
|
integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==
|
||||||
|
|
||||||
"@esbuild/linux-arm64@0.27.3":
|
"@esbuild/linux-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d"
|
||||||
integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==
|
integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==
|
||||||
|
|
||||||
"@esbuild/linux-arm@0.27.3":
|
"@esbuild/linux-arm@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753"
|
||||||
integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==
|
integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==
|
||||||
|
|
||||||
"@esbuild/linux-ia32@0.27.3":
|
"@esbuild/linux-ia32@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b"
|
||||||
integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==
|
integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.27.3":
|
"@esbuild/linux-loong64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98"
|
||||||
integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==
|
integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@0.27.3":
|
"@esbuild/linux-mips64el@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca"
|
||||||
integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==
|
integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@0.27.3":
|
"@esbuild/linux-ppc64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2"
|
||||||
integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==
|
integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@0.27.3":
|
"@esbuild/linux-riscv64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27"
|
||||||
integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==
|
integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==
|
||||||
|
|
||||||
"@esbuild/linux-s390x@0.27.3":
|
"@esbuild/linux-s390x@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb"
|
||||||
integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==
|
integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==
|
||||||
|
|
||||||
"@esbuild/linux-x64@0.27.3":
|
"@esbuild/linux-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5"
|
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485"
|
||||||
integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==
|
integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64@0.27.3":
|
"@esbuild/netbsd-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7"
|
resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1"
|
||||||
integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==
|
integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@0.27.3":
|
"@esbuild/netbsd-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b"
|
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de"
|
||||||
integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==
|
integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64@0.27.3":
|
"@esbuild/openbsd-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5"
|
resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef"
|
||||||
integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==
|
integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@0.27.3":
|
"@esbuild/openbsd-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b"
|
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd"
|
||||||
integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==
|
integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64@0.27.3":
|
"@esbuild/openharmony-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e"
|
resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce"
|
||||||
integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==
|
integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==
|
||||||
|
|
||||||
"@esbuild/sunos-x64@0.27.3":
|
"@esbuild/sunos-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537"
|
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4"
|
||||||
integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==
|
integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==
|
||||||
|
|
||||||
"@esbuild/win32-arm64@0.27.3":
|
"@esbuild/win32-arm64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e"
|
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532"
|
||||||
integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==
|
integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==
|
||||||
|
|
||||||
"@esbuild/win32-ia32@0.27.3":
|
"@esbuild/win32-ia32@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c"
|
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7"
|
||||||
integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==
|
integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==
|
||||||
|
|
||||||
"@esbuild/win32-x64@0.27.3":
|
"@esbuild/win32-x64@0.28.2":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17"
|
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87"
|
||||||
integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==
|
integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==
|
||||||
|
|
||||||
"@floating-ui/core@^1.7.5":
|
"@floating-ui/core@^1.7.5":
|
||||||
version "1.7.5"
|
version "1.7.5"
|
||||||
@@ -808,6 +862,17 @@
|
|||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
"@vue/compiler-core@3.5.42":
|
||||||
|
version "3.5.42"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz#9d159ca5b1e7d792a42e9b1ddf2fc31edc9333dc"
|
||||||
|
integrity sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.29.8"
|
||||||
|
"@vue/shared" "3.5.42"
|
||||||
|
entities "^7.0.1"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
"@vue/compiler-dom@3.5.29":
|
"@vue/compiler-dom@3.5.29":
|
||||||
version "3.5.29"
|
version "3.5.29"
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz#055cf5492005249591c7fb45868a874468e4ffb3"
|
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz#055cf5492005249591c7fb45868a874468e4ffb3"
|
||||||
@@ -816,6 +881,14 @@
|
|||||||
"@vue/compiler-core" "3.5.29"
|
"@vue/compiler-core" "3.5.29"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.29"
|
||||||
|
|
||||||
|
"@vue/compiler-dom@3.5.42":
|
||||||
|
version "3.5.42"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz#2ac9570010bbffb7c08d8a904a956ecbabcdf6c6"
|
||||||
|
integrity sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-core" "3.5.42"
|
||||||
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
"@vue/compiler-dom@^3.5.13":
|
"@vue/compiler-dom@^3.5.13":
|
||||||
version "3.5.30"
|
version "3.5.30"
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz#a38dbdd520479244c8b673123b4bd06a82e733ee"
|
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz#a38dbdd520479244c8b673123b4bd06a82e733ee"
|
||||||
@@ -824,7 +897,22 @@
|
|||||||
"@vue/compiler-core" "3.5.30"
|
"@vue/compiler-core" "3.5.30"
|
||||||
"@vue/shared" "3.5.30"
|
"@vue/shared" "3.5.30"
|
||||||
|
|
||||||
"@vue/compiler-sfc@3.5.29", "@vue/compiler-sfc@^3.5.22":
|
"@vue/compiler-sfc@3.5.42":
|
||||||
|
version "3.5.42"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz#502c50e7864a980b7f147b78974e3b49fefcc16d"
|
||||||
|
integrity sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/parser" "^7.29.8"
|
||||||
|
"@vue/compiler-core" "3.5.42"
|
||||||
|
"@vue/compiler-dom" "3.5.42"
|
||||||
|
"@vue/compiler-ssr" "3.5.42"
|
||||||
|
"@vue/shared" "3.5.42"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
magic-string "^0.30.21"
|
||||||
|
postcss "^8.5.19"
|
||||||
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
|
"@vue/compiler-sfc@^3.5.22":
|
||||||
version "3.5.29"
|
version "3.5.29"
|
||||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz#8b1b0707fb53c122fedd566244a564eaf40ee71f"
|
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz#8b1b0707fb53c122fedd566244a564eaf40ee71f"
|
||||||
integrity sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==
|
integrity sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==
|
||||||
@@ -847,43 +935,52 @@
|
|||||||
"@vue/compiler-dom" "3.5.29"
|
"@vue/compiler-dom" "3.5.29"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.29"
|
||||||
|
|
||||||
|
"@vue/compiler-ssr@3.5.42":
|
||||||
|
version "3.5.42"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz#a3121ad517e9062121762e452f125674cc6162fa"
|
||||||
|
integrity sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==
|
||||||
|
dependencies:
|
||||||
|
"@vue/compiler-dom" "3.5.42"
|
||||||
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
"@vue/devtools-api@^6.6.4":
|
"@vue/devtools-api@^6.6.4":
|
||||||
version "6.6.4"
|
version "6.6.4"
|
||||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||||
|
|
||||||
"@vue/reactivity@3.5.29":
|
"@vue/reactivity@3.5.42":
|
||||||
version "3.5.29"
|
version "3.5.42"
|
||||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.29.tgz#68fbaedcc2584328060edceedc4d4370b49c9fab"
|
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.42.tgz#800220f023d36fd04d94460c7c7ab5808f8ae75f"
|
||||||
integrity sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==
|
integrity sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
"@vue/runtime-core@3.5.29":
|
"@vue/runtime-core@3.5.42":
|
||||||
version "3.5.29"
|
version "3.5.42"
|
||||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.29.tgz#fbd34110aa47e74a22fe430734018bec56913b40"
|
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.42.tgz#a8af85746d6f9a9f1f8e4626be24eb0b8c9a1e72"
|
||||||
integrity sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==
|
integrity sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/reactivity" "3.5.29"
|
"@vue/reactivity" "3.5.42"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
"@vue/runtime-dom@3.5.29":
|
"@vue/runtime-dom@3.5.42":
|
||||||
version "3.5.29"
|
version "3.5.42"
|
||||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz#4eb686b22178f13c262ea1d0d7171a3134ced21f"
|
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz#c7b487a92b2c381db547c1aa5040bd7febb40448"
|
||||||
integrity sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==
|
integrity sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/reactivity" "3.5.29"
|
"@vue/reactivity" "3.5.42"
|
||||||
"@vue/runtime-core" "3.5.29"
|
"@vue/runtime-core" "3.5.42"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.42"
|
||||||
csstype "^3.2.3"
|
csstype "^3.2.3"
|
||||||
|
|
||||||
"@vue/server-renderer@3.5.29":
|
"@vue/server-renderer@3.5.42":
|
||||||
version "3.5.29"
|
version "3.5.42"
|
||||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.29.tgz#9cbcbf676b2976fbb8171d5de79501894ba84cad"
|
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.42.tgz#27c3d4743273bf7e839fc5a5e2f7db25e8d779e8"
|
||||||
integrity sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==
|
integrity sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-ssr" "3.5.29"
|
"@vue/compiler-ssr" "3.5.42"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/runtime-dom" "3.5.42"
|
||||||
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
"@vue/shared@3.5.29", "@vue/shared@^3.5.22":
|
"@vue/shared@3.5.29", "@vue/shared@^3.5.22":
|
||||||
version "3.5.29"
|
version "3.5.29"
|
||||||
@@ -895,9 +992,14 @@
|
|||||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz#5d7a0d3ca151647484303fd9f057e2e13ecb80ef"
|
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz#5d7a0d3ca151647484303fd9f057e2e13ecb80ef"
|
||||||
integrity sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==
|
integrity sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==
|
||||||
|
|
||||||
"@wailsio/runtime@latest":
|
"@vue/shared@3.5.42":
|
||||||
|
version "3.5.42"
|
||||||
|
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.42.tgz#a413cf21262502662354cda3b65ab31bca979202"
|
||||||
|
integrity sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==
|
||||||
|
|
||||||
|
"@wailsio/runtime@3.0.0-alpha.79":
|
||||||
version "3.0.0-alpha.79"
|
version "3.0.0-alpha.79"
|
||||||
resolved "https://registry.yarnpkg.com/@wailsio/runtime/-/runtime-3.0.0-alpha.79.tgz#d43310f5b87dc46142756abf0973ba07277be66e"
|
resolved "https://registry.npmmirror.com/@wailsio/runtime/-/runtime-3.0.0-alpha.79.tgz#d43310f5b87dc46142756abf0973ba07277be66e"
|
||||||
integrity sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==
|
integrity sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==
|
||||||
|
|
||||||
ansi-styles@^4.1.0:
|
ansi-styles@^4.1.0:
|
||||||
@@ -941,15 +1043,10 @@ autoprefixer@^10.5.0:
|
|||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
postcss-value-parser "^4.2.0"
|
postcss-value-parser "^4.2.0"
|
||||||
|
|
||||||
baseline-browser-mapping@^2.10.12:
|
baseline-browser-mapping@2.11.0, baseline-browser-mapping@^2.10.44:
|
||||||
version "2.10.28"
|
version "2.11.0"
|
||||||
resolved "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz#9ba8e7b5ef9d40ceb3dd1cdcb936f63383b6833b"
|
resolved "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz#25453328838032c7a514fbd238a171f744e9e854"
|
||||||
integrity sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==
|
integrity sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==
|
||||||
|
|
||||||
baseline-browser-mapping@^2.9.0:
|
|
||||||
version "2.10.0"
|
|
||||||
resolved "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9"
|
|
||||||
integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==
|
|
||||||
|
|
||||||
binary-extensions@^2.0.0:
|
binary-extensions@^2.0.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
@@ -963,26 +1060,15 @@ braces@^3.0.3, braces@~3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.1.1"
|
fill-range "^7.1.1"
|
||||||
|
|
||||||
browserslist@^4.24.0:
|
browserslist@4.28.7, browserslist@^4.24.0, browserslist@^4.28.2:
|
||||||
version "4.28.1"
|
version "4.28.7"
|
||||||
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
|
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz#409046517fccd2e51cdc20f077454b7141184028"
|
||||||
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
|
integrity sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping "^2.9.0"
|
baseline-browser-mapping "^2.10.44"
|
||||||
caniuse-lite "^1.0.30001759"
|
caniuse-lite "^1.0.30001806"
|
||||||
electron-to-chromium "^1.5.263"
|
electron-to-chromium "^1.5.393"
|
||||||
node-releases "^2.0.27"
|
node-releases "^2.0.51"
|
||||||
update-browserslist-db "^1.2.0"
|
|
||||||
|
|
||||||
browserslist@^4.28.2:
|
|
||||||
version "4.28.2"
|
|
||||||
resolved "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2"
|
|
||||||
integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==
|
|
||||||
dependencies:
|
|
||||||
baseline-browser-mapping "^2.10.12"
|
|
||||||
caniuse-lite "^1.0.30001782"
|
|
||||||
electron-to-chromium "^1.5.328"
|
|
||||||
node-releases "^2.0.36"
|
|
||||||
update-browserslist-db "^1.2.3"
|
update-browserslist-db "^1.2.3"
|
||||||
|
|
||||||
camelcase-css@^2.0.1:
|
camelcase-css@^2.0.1:
|
||||||
@@ -990,16 +1076,16 @@ camelcase-css@^2.0.1:
|
|||||||
resolved "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
resolved "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001759:
|
caniuse-lite@^1.0.30001787:
|
||||||
version "1.0.30001775"
|
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz#9572266e3f7f77efee5deac1efeb4795879d1b7f"
|
|
||||||
integrity sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==
|
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001782, caniuse-lite@^1.0.30001787:
|
|
||||||
version "1.0.30001792"
|
version "1.0.30001792"
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
|
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
|
||||||
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
|
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001806:
|
||||||
|
version "1.0.30001823"
|
||||||
|
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz#331a7cb49aa317517697e76b0b1295f2225959fe"
|
||||||
|
integrity sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==
|
||||||
|
|
||||||
chalk@4.1.1:
|
chalk@4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
|
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
|
||||||
@@ -1115,15 +1201,10 @@ dotenv@^16.1.4:
|
|||||||
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
|
resolved "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020"
|
||||||
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
|
integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==
|
||||||
|
|
||||||
electron-to-chromium@^1.5.263:
|
electron-to-chromium@^1.5.393:
|
||||||
version "1.5.302"
|
version "1.5.478"
|
||||||
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb"
|
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz#e1d6d5279242d2f90fa287a8343dc04ddc0c52ec"
|
||||||
integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==
|
integrity sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==
|
||||||
|
|
||||||
electron-to-chromium@^1.5.328:
|
|
||||||
version "1.5.353"
|
|
||||||
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz#01e8a8e25a0bf13e631106045f177d0568ca91c2"
|
|
||||||
integrity sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==
|
|
||||||
|
|
||||||
entities@^7.0.1:
|
entities@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
@@ -1135,37 +1216,37 @@ es-errors@^1.3.0:
|
|||||||
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||||
|
|
||||||
esbuild@^0.27.0:
|
"esbuild@^0.27.0 || ^0.28.0":
|
||||||
version "0.27.3"
|
version "0.28.2"
|
||||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8"
|
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816"
|
||||||
integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==
|
integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@esbuild/aix-ppc64" "0.27.3"
|
"@esbuild/aix-ppc64" "0.28.2"
|
||||||
"@esbuild/android-arm" "0.27.3"
|
"@esbuild/android-arm" "0.28.2"
|
||||||
"@esbuild/android-arm64" "0.27.3"
|
"@esbuild/android-arm64" "0.28.2"
|
||||||
"@esbuild/android-x64" "0.27.3"
|
"@esbuild/android-x64" "0.28.2"
|
||||||
"@esbuild/darwin-arm64" "0.27.3"
|
"@esbuild/darwin-arm64" "0.28.2"
|
||||||
"@esbuild/darwin-x64" "0.27.3"
|
"@esbuild/darwin-x64" "0.28.2"
|
||||||
"@esbuild/freebsd-arm64" "0.27.3"
|
"@esbuild/freebsd-arm64" "0.28.2"
|
||||||
"@esbuild/freebsd-x64" "0.27.3"
|
"@esbuild/freebsd-x64" "0.28.2"
|
||||||
"@esbuild/linux-arm" "0.27.3"
|
"@esbuild/linux-arm" "0.28.2"
|
||||||
"@esbuild/linux-arm64" "0.27.3"
|
"@esbuild/linux-arm64" "0.28.2"
|
||||||
"@esbuild/linux-ia32" "0.27.3"
|
"@esbuild/linux-ia32" "0.28.2"
|
||||||
"@esbuild/linux-loong64" "0.27.3"
|
"@esbuild/linux-loong64" "0.28.2"
|
||||||
"@esbuild/linux-mips64el" "0.27.3"
|
"@esbuild/linux-mips64el" "0.28.2"
|
||||||
"@esbuild/linux-ppc64" "0.27.3"
|
"@esbuild/linux-ppc64" "0.28.2"
|
||||||
"@esbuild/linux-riscv64" "0.27.3"
|
"@esbuild/linux-riscv64" "0.28.2"
|
||||||
"@esbuild/linux-s390x" "0.27.3"
|
"@esbuild/linux-s390x" "0.28.2"
|
||||||
"@esbuild/linux-x64" "0.27.3"
|
"@esbuild/linux-x64" "0.28.2"
|
||||||
"@esbuild/netbsd-arm64" "0.27.3"
|
"@esbuild/netbsd-arm64" "0.28.2"
|
||||||
"@esbuild/netbsd-x64" "0.27.3"
|
"@esbuild/netbsd-x64" "0.28.2"
|
||||||
"@esbuild/openbsd-arm64" "0.27.3"
|
"@esbuild/openbsd-arm64" "0.28.2"
|
||||||
"@esbuild/openbsd-x64" "0.27.3"
|
"@esbuild/openbsd-x64" "0.28.2"
|
||||||
"@esbuild/openharmony-arm64" "0.27.3"
|
"@esbuild/openharmony-arm64" "0.28.2"
|
||||||
"@esbuild/sunos-x64" "0.27.3"
|
"@esbuild/sunos-x64" "0.28.2"
|
||||||
"@esbuild/win32-arm64" "0.27.3"
|
"@esbuild/win32-arm64" "0.28.2"
|
||||||
"@esbuild/win32-ia32" "0.27.3"
|
"@esbuild/win32-ia32" "0.28.2"
|
||||||
"@esbuild/win32-x64" "0.27.3"
|
"@esbuild/win32-x64" "0.28.2"
|
||||||
|
|
||||||
escalade@^3.2.0:
|
escalade@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
@@ -1363,20 +1444,15 @@ mz@^2.7.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nanoid@^3.3.11:
|
nanoid@3.3.18, nanoid@^3.3.16:
|
||||||
version "3.3.11"
|
version "3.3.18"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913"
|
||||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==
|
||||||
|
|
||||||
node-releases@^2.0.27:
|
node-releases@^2.0.51:
|
||||||
version "2.0.27"
|
version "2.0.63"
|
||||||
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.63.tgz#ad6035ec7745f8bc3b14f6500cd70d5545634612"
|
||||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
integrity sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==
|
||||||
|
|
||||||
node-releases@^2.0.36:
|
|
||||||
version "2.0.38"
|
|
||||||
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz#791569b9e4424a044e12c3abfad418ed83ce9947"
|
|
||||||
integrity sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==
|
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
@@ -1408,21 +1484,16 @@ picocolors@^1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
|
picomatch@4.0.4, picomatch@^4.0.3, picomatch@^4.0.4:
|
||||||
|
version "4.0.4"
|
||||||
|
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||||
|
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||||
version "2.3.2"
|
version "2.3.2"
|
||||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
|
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
|
||||||
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
||||||
|
|
||||||
picomatch@^4.0.3:
|
|
||||||
version "4.0.3"
|
|
||||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
|
||||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
|
||||||
|
|
||||||
picomatch@^4.0.4:
|
|
||||||
version "4.0.4"
|
|
||||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
|
||||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
|
||||||
|
|
||||||
pify@^2.3.0:
|
pify@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
@@ -1471,10 +1542,10 @@ postcss-nested@^6.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser "^6.1.1"
|
postcss-selector-parser "^6.1.1"
|
||||||
|
|
||||||
postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2:
|
postcss-selector-parser@6.1.5, postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2:
|
||||||
version "6.1.2"
|
version "6.1.5"
|
||||||
resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de"
|
resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz#20e988ce8eab87575d54824d9800236c5fd872ca"
|
||||||
integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
|
integrity sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
util-deprecate "^1.0.2"
|
util-deprecate "^1.0.2"
|
||||||
@@ -1484,21 +1555,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@^8.4.47, postcss@^8.5.14:
|
postcss@8.5.23, postcss@^8.4.47, postcss@^8.5.19, postcss@^8.5.6:
|
||||||
version "8.5.14"
|
version "8.5.23"
|
||||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
|
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594"
|
||||||
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
|
integrity sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.11"
|
nanoid "^3.3.16"
|
||||||
picocolors "^1.1.1"
|
|
||||||
source-map-js "^1.2.1"
|
|
||||||
|
|
||||||
postcss@^8.5.6:
|
|
||||||
version "8.5.6"
|
|
||||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
|
||||||
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
|
|
||||||
dependencies:
|
|
||||||
nanoid "^3.3.11"
|
|
||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
source-map-js "^1.2.1"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
@@ -1587,10 +1649,10 @@ semver@^6.3.1:
|
|||||||
resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
source-map-js@^1.2.1:
|
source-map-js@1.2.2, source-map-js@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab"
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==
|
||||||
|
|
||||||
sucrase@^3.35.0:
|
sucrase@^3.35.0:
|
||||||
version "3.35.1"
|
version "3.35.1"
|
||||||
@@ -1687,7 +1749,7 @@ ts-interface-checker@^0.1.9:
|
|||||||
resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||||
|
|
||||||
update-browserslist-db@^1.2.0, update-browserslist-db@^1.2.3:
|
update-browserslist-db@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
|
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
|
||||||
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
|
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
|
||||||
@@ -1700,10 +1762,10 @@ util-deprecate@^1.0.2:
|
|||||||
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0, uuid@11.1.1:
|
||||||
version "10.0.0"
|
version "11.1.1"
|
||||||
resolved "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
resolved "https://registry.npmmirror.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad"
|
||||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==
|
||||||
|
|
||||||
vite-plugin-top-level-await@^1.6.0:
|
vite-plugin-top-level-await@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
@@ -1715,12 +1777,12 @@ vite-plugin-top-level-await@^1.6.0:
|
|||||||
"@swc/wasm" "^1.12.14"
|
"@swc/wasm" "^1.12.14"
|
||||||
uuid "10.0.0"
|
uuid "10.0.0"
|
||||||
|
|
||||||
vite@^7.1.11:
|
vite@7.3.7:
|
||||||
version "7.3.1"
|
version "7.3.7"
|
||||||
resolved "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507"
|
resolved "https://registry.npmmirror.com/vite/-/vite-7.3.7.tgz#017a80fdce051003a4aa290dddd0b159aa769282"
|
||||||
integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==
|
integrity sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.27.0"
|
esbuild "^0.27.0 || ^0.28.0"
|
||||||
fdir "^6.5.0"
|
fdir "^6.5.0"
|
||||||
picomatch "^4.0.3"
|
picomatch "^4.0.3"
|
||||||
postcss "^8.5.6"
|
postcss "^8.5.6"
|
||||||
@@ -1741,16 +1803,16 @@ vue-router@^4.6.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@vue/devtools-api" "^6.6.4"
|
"@vue/devtools-api" "^6.6.4"
|
||||||
|
|
||||||
vue@^3.5.22:
|
vue@3.5.42:
|
||||||
version "3.5.29"
|
version "3.5.42"
|
||||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz#859a1cc5219eb1228c7ce6f355b27de080517111"
|
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz#6099e750a19c68a5c09f785808d5abb01558a3c1"
|
||||||
integrity sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==
|
integrity sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-dom" "3.5.29"
|
"@vue/compiler-dom" "3.5.42"
|
||||||
"@vue/compiler-sfc" "3.5.29"
|
"@vue/compiler-sfc" "3.5.42"
|
||||||
"@vue/runtime-dom" "3.5.29"
|
"@vue/runtime-dom" "3.5.42"
|
||||||
"@vue/server-renderer" "3.5.29"
|
"@vue/server-renderer" "3.5.42"
|
||||||
"@vue/shared" "3.5.29"
|
"@vue/shared" "3.5.42"
|
||||||
|
|
||||||
yallist@^3.0.2:
|
yallist@^3.0.2:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
|
|||||||
20
go.mod
20
go.mod
@@ -1,6 +1,8 @@
|
|||||||
module cursor
|
module cursor
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.10
|
||||||
|
|
||||||
require (
|
require (
|
||||||
codeberg.org/readeck/go-readability/v2 v2.1.1
|
codeberg.org/readeck/go-readability/v2 v2.1.1
|
||||||
@@ -17,8 +19,8 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/sergi/go-diff v1.4.0
|
github.com/sergi/go-diff v1.4.0
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
|
||||||
golang.org/x/net v0.49.0
|
golang.org/x/net v0.62.0
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/sys v0.49.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.50.1
|
modernc.org/sqlite v1.50.1
|
||||||
@@ -40,8 +42,8 @@ require (
|
|||||||
github.com/ebitengine/purego v0.9.1 // indirect
|
github.com/ebitengine/purego v0.9.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
github.com/go-git/go-git/v5 v5.19.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
|
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
@@ -56,7 +58,7 @@ require (
|
|||||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
@@ -64,9 +66,9 @@ require (
|
|||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.58.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.24.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.43.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.72.3 // indirect
|
modernc.org/libc v1.72.3 // indirect
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -52,12 +52,12 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
|||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
@@ -120,8 +120,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
|||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -168,17 +168,17 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
@@ -189,8 +189,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -198,8 +198,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -218,8 +218,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -229,8 +229,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -241,16 +241,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/leaanthony/u"
|
"github.com/leaanthony/u"
|
||||||
|
|
||||||
|
"cursor/internal/appdata"
|
||||||
bridge "cursor/internal/bridge"
|
bridge "cursor/internal/bridge"
|
||||||
"cursor/internal/certs"
|
"cursor/internal/certs"
|
||||||
"cursor/internal/cursor"
|
"cursor/internal/cursor"
|
||||||
@@ -32,6 +33,8 @@ const (
|
|||||||
|
|
||||||
// EmbeddedResources 定义了当前模块中的 EmbeddedResources 类型。
|
// EmbeddedResources 定义了当前模块中的 EmbeddedResources 类型。
|
||||||
type EmbeddedResources struct {
|
type EmbeddedResources struct {
|
||||||
|
// DisableAutoStart opens the UI without changing proxy settings or certificate trust.
|
||||||
|
DisableAutoStart bool
|
||||||
// Assets 表示当前声明中的 Assets。
|
// Assets 表示当前声明中的 Assets。
|
||||||
Assets fs.FS
|
Assets fs.FS
|
||||||
// AppIcon 表示当前声明中的 AppIcon。
|
// AppIcon 表示当前声明中的 AppIcon。
|
||||||
@@ -50,17 +53,18 @@ func init() {
|
|||||||
// Run 用于处理与 Run 相关的逻辑。
|
// Run 用于处理与 Run 相关的逻辑。
|
||||||
func Run(resources EmbeddedResources) error {
|
func Run(resources EmbeddedResources) error {
|
||||||
logger.Init()
|
logger.Init()
|
||||||
|
if !resources.DisableAutoStart {
|
||||||
ensureStartMenuShortcut()
|
ensureStartMenuShortcut()
|
||||||
|
}
|
||||||
netproxy.InstallDefaultTransport()
|
netproxy.InstallDefaultTransport()
|
||||||
|
|
||||||
embeddedCACertPEM := certs.EmbeddedCACertPEM()
|
certManager, err := certs.LoadOrCreateLocalCA(appdata.DataRootPath())
|
||||||
logEmbeddedCAInfo(embeddedCACertPEM)
|
|
||||||
|
|
||||||
certManager, err := certs.NewEmbeddedManager()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
embeddedCACertPEM := certManager.CACertPEM()
|
||||||
|
logEmbeddedCAInfo(embeddedCACertPEM)
|
||||||
defaultBackendBaseURL := "http://" + serverconfig.DefaultBackendListenAddr
|
defaultBackendBaseURL := "http://" + serverconfig.DefaultBackendListenAddr
|
||||||
proxyServer, err := mitm.NewProxyServer(serverconfig.DefaultProxyListenAddr, defaultBackendBaseURL, "", "", certManager)
|
proxyServer, err := mitm.NewProxyServer(serverconfig.DefaultProxyListenAddr, defaultBackendBaseURL, "", "", certManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,7 +94,12 @@ func Run(resources EmbeddedResources) error {
|
|||||||
proxyService.ShutdownForQuit()
|
proxyService.ShutdownForQuit()
|
||||||
},
|
},
|
||||||
SingleInstance: &application.SingleInstanceOptions{
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
UniqueID: "com.local-curosr.single-instance",
|
UniqueID: func() string {
|
||||||
|
if resources.DisableAutoStart {
|
||||||
|
return "com.local-cursor.readonly-ui"
|
||||||
|
}
|
||||||
|
return "com.local-curosr.single-instance"
|
||||||
|
}(),
|
||||||
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||||
logger.Infof("检测到实例请求,已忽略")
|
logger.Infof("检测到实例请求,已忽略")
|
||||||
// 不激活窗口,避免干扰用户工作
|
// 不激活窗口,避免干扰用户工作
|
||||||
@@ -200,6 +209,9 @@ func Run(resources EmbeddedResources) error {
|
|||||||
})
|
})
|
||||||
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
|
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
|
||||||
logger.Infof("应用版本:v%s", buildinfo.CurrentVersion())
|
logger.Infof("应用版本:v%s", buildinfo.CurrentVersion())
|
||||||
|
if resources.DisableAutoStart {
|
||||||
|
return
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
logger.Infof("application started, begin auto start service in background")
|
logger.Infof("application started, begin auto start service in background")
|
||||||
if _, err := proxyService.StartProxy(); err != nil {
|
if _, err := proxyService.StartProxy(); err != nil {
|
||||||
@@ -244,17 +256,17 @@ func Run(resources EmbeddedResources) error {
|
|||||||
// logEmbeddedCAInfo 用于处理与 logEmbeddedCAInfo 相关的逻辑。
|
// logEmbeddedCAInfo 用于处理与 logEmbeddedCAInfo 相关的逻辑。
|
||||||
func logEmbeddedCAInfo(certPEM []byte) {
|
func logEmbeddedCAInfo(certPEM []byte) {
|
||||||
if len(certPEM) == 0 {
|
if len(certPEM) == 0 {
|
||||||
logger.Errorf("embedded CA is empty")
|
logger.Errorf("local CA is empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cert, err := parseEmbeddedCert(certPEM)
|
cert, err := parseEmbeddedCert(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("parse embedded CA failed: %v", err)
|
logger.Errorf("parse local CA failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sum := sha256.Sum256(cert.Raw)
|
sum := sha256.Sum256(cert.Raw)
|
||||||
logger.Infof(
|
logger.Infof(
|
||||||
"embedded CA loaded: sha256=%s subject=%s valid=%s~%s",
|
"local CA loaded: sha256=%s subject=%s valid=%s~%s",
|
||||||
strings.ToUpper(hex.EncodeToString(sum[:])),
|
strings.ToUpper(hex.EncodeToString(sum[:])),
|
||||||
cert.Subject.String(),
|
cert.Subject.String(),
|
||||||
cert.NotBefore.Format(time.RFC3339),
|
cert.NotBefore.Format(time.RFC3339),
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ func ensureStartMenuShortcut() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(lnkPath), 0700); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
workDir := filepath.Dir(exePath)
|
workDir := filepath.Dir(exePath)
|
||||||
psScript := `$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('` + lnkPath + `'); $s.TargetPath = '` + exePath + `'; $s.WorkingDirectory = '` + workDir + `'; $s.Description = 'Local-Cursor'; $s.Save()`
|
psScript := `$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('` + strings.ReplaceAll(lnkPath, "'", "''") + `'); $s.TargetPath = '` + strings.ReplaceAll(exePath, "'", "''") + `'; $s.WorkingDirectory = '` + strings.ReplaceAll(workDir, "'", "''") + `'; $s.Description = 'Local-Cursor'; $s.Save()`
|
||||||
cmd := winexec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript)
|
cmd := winexec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
logger.Infof("ensureStartMenuShortcut: create failed lnk=%s err=%v", lnkPath, err)
|
logger.Infof("ensureStartMenuShortcut: create failed lnk=%s err=%v", lnkPath, err)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package appdata
|
package appdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cursor/internal/securefile"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,19 +10,19 @@ import (
|
|||||||
|
|
||||||
func ensureAssistantHome() error {
|
func ensureAssistantHome() error {
|
||||||
migrateLegacyAssistantHome()
|
migrateLegacyAssistantHome()
|
||||||
if err := os.MkdirAll(RootDir(), 0o755); err != nil {
|
if err := securefile.PrivateDir(RootDir()); err != nil {
|
||||||
return fmt.Errorf("create assistant home: %w", err)
|
return fmt.Errorf("create assistant home: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(DataRootPath(), 0o755); err != nil {
|
if err := securefile.PrivateDir(DataRootPath()); err != nil {
|
||||||
return fmt.Errorf("create data root: %w", err)
|
return fmt.Errorf("create data root: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(HistoryRootPath(), 0o755); err != nil {
|
if err := securefile.PrivateDir(HistoryRootPath()); err != nil {
|
||||||
return fmt.Errorf("create history root: %w", err)
|
return fmt.Errorf("create history root: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(RulesRootPath(), 0o755); err != nil {
|
if err := securefile.PrivateDir(RulesRootPath()); err != nil {
|
||||||
return fmt.Errorf("create rules root: %w", err)
|
return fmt.Errorf("create rules root: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(LogsRootPath(), 0o755); err != nil {
|
if err := securefile.PrivateDir(LogsRootPath()); err != nil {
|
||||||
return fmt.Errorf("create logs root: %w", err)
|
return fmt.Errorf("create logs root: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
`internal/backend` 当前支持本地助手模式与直连上游模式。
|
`internal/backend` 当前支持本地助手模式与直连上游模式。
|
||||||
|
|
||||||
关于 backend agent「最小事实集合」的第一阶段研究文档,见 [`../../docs/backend-agent-minimum-facts-phase1.md`](../../docs/backend-agent-minimum-facts-phase1.md)。
|
|
||||||
|
|
||||||
核心边界:
|
核心边界:
|
||||||
|
|
||||||
@@ -98,7 +97,6 @@ internal/backend/
|
|||||||
|
|
||||||
- `~/.cursor-local-assistant-v2/config.yaml`
|
- `~/.cursor-local-assistant-v2/config.yaml`
|
||||||
- `~/.cursor-local-assistant-v2/data/ca.crt`
|
- `~/.cursor-local-assistant-v2/data/ca.crt`
|
||||||
- `~/.cursor-local-assistant-v2/data/ads/`
|
|
||||||
- `~/.cursor-local-assistant-v2/history/`
|
- `~/.cursor-local-assistant-v2/history/`
|
||||||
- `~/.cursor-local-assistant-v2/logs/`
|
- `~/.cursor-local-assistant-v2/logs/`
|
||||||
|
|
||||||
@@ -106,7 +104,6 @@ internal/backend/
|
|||||||
|
|
||||||
- `config.yaml` 是用户配置
|
- `config.yaml` 是用户配置
|
||||||
- `data/ca.crt` 是注入给宿主的 CA 证书
|
- `data/ca.crt` 是注入给宿主的 CA 证书
|
||||||
- `data/ads/` 是广告包与资源缓存目录
|
|
||||||
- `history/` 是会话事实与全局 usage JSON 目录,不属于日志
|
- `history/` 是会话事实与全局 usage JSON 目录,不属于日志
|
||||||
- `logs/` 只保留必要文本运行日志
|
- `logs/` 只保留必要文本运行日志
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -325,7 +326,7 @@ func (bridge *Bridge) openSwitchMode(toolCall runtimecore.ToolInvocation) (*agen
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
argsPayload, _ := json.Marshal(args)
|
argsPayload, _ := json.Marshal(&args)
|
||||||
return serverMessage, runtimecore.PendingInteraction{
|
return serverMessage, runtimecore.PendingInteraction{
|
||||||
InteractionID: fmt.Sprintf("%d", messageID),
|
InteractionID: fmt.Sprintf("%d", messageID),
|
||||||
ArgsJSON: argsPayload,
|
ArgsJSON: argsPayload,
|
||||||
@@ -679,7 +680,7 @@ func truncateWebSearchReplay(searchTerm string, references []*agentv1.WebSearchR
|
|||||||
if reference == nil {
|
if reference == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
next := *reference
|
next := proto.Clone(reference).(*agentv1.WebSearchReference)
|
||||||
title := truncateInteractionText("WebSearch title", next.GetTitle(), webSearchTitleLimit)
|
title := truncateInteractionText("WebSearch title", next.GetTitle(), webSearchTitleLimit)
|
||||||
chunk := truncateInteractionText("WebSearch snippet", next.GetChunk(), webSearchChunkLimit)
|
chunk := truncateInteractionText("WebSearch snippet", next.GetChunk(), webSearchChunkLimit)
|
||||||
if title != next.GetTitle() || chunk != next.GetChunk() {
|
if title != next.GetTitle() || chunk != next.GetChunk() {
|
||||||
@@ -687,7 +688,7 @@ func truncateWebSearchReplay(searchTerm string, references []*agentv1.WebSearchR
|
|||||||
}
|
}
|
||||||
next.Title = title
|
next.Title = title
|
||||||
next.Chunk = chunk
|
next.Chunk = chunk
|
||||||
nextReferences = append(nextReferences, &next)
|
nextReferences = append(nextReferences, next)
|
||||||
}
|
}
|
||||||
nextPayload := formatWebSearchPayload(searchTerm, nextReferences)
|
nextPayload := formatWebSearchPayload(searchTerm, nextReferences)
|
||||||
if strings.TrimSpace(payload) != "" && len(nextPayload) == 0 {
|
if strings.TrimSpace(payload) != "" && len(nextPayload) == 0 {
|
||||||
|
|||||||
94
internal/backend/agent/model/http_integration_test.go
Normal file
94
internal/backend/agent/model/http_integration_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package modeladapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvidersForwardAuthenticationOverridesAndFinishStream(t *testing.T) {
|
||||||
|
for _, provider := range []string{"openai", "anthropic"} {
|
||||||
|
t.Run(provider, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("X-Gateway-Key") != "fixture-gateway" || r.Host != "gateway.fixture" {
|
||||||
|
t.Error("custom headers not applied")
|
||||||
|
}
|
||||||
|
if provider == "openai" && r.Header.Get("Authorization") != "Bearer fixture-key" {
|
||||||
|
t.Error("OpenAI authentication missing")
|
||||||
|
}
|
||||||
|
if provider == "anthropic" && r.Header.Get("x-api-key") != "fixture-key" {
|
||||||
|
t.Error("Anthropic authentication missing")
|
||||||
|
}
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if body["model"] != "fixture-model" {
|
||||||
|
t.Error("model mapping lost")
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
if provider == "openai" {
|
||||||
|
fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"ok\"},\"finish_reason\":null}]}\n\ndata: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1}}\n\ndata: [DONE]\n\n")
|
||||||
|
} else {
|
||||||
|
for _, event := range []string{`{"type":"message_start","message":{"id":"fixture","type":"message","role":"assistant","content":[],"model":"fixture-model","usage":{"input_tokens":1,"output_tokens":0}}}`, `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`, `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`, `{"type":"content_block_stop","index":0}`, `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":1}}`, `{"type":"message_stop"}`} {
|
||||||
|
var envelope struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal([]byte(event), &envelope)
|
||||||
|
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", envelope.Type, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
req := StreamRequest{BaseURL: server.URL, APIKey: "fixture-key", ProviderModelID: "fixture-model", ModelID: "fixture-model", OpenAIEndpoint: "/v1/chat/completions", Messages: []Message{{Role: "user", Content: "hello"}}, MaxTokens: 32, Stream: true, CustomHeadersEnabled: true, CustomHeadersJSON: `{"X-Gateway-Key":"fixture-gateway","Host":"gateway.fixture"}`}
|
||||||
|
var text strings.Builder
|
||||||
|
finished := false
|
||||||
|
sink := func(event ModelEvent) error {
|
||||||
|
if event.Kind == ModelEventKindTextDelta {
|
||||||
|
text.WriteString(event.Text)
|
||||||
|
}
|
||||||
|
if event.Kind == ModelEventKindTurnFinished {
|
||||||
|
finished = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
var err error
|
||||||
|
if provider == "openai" {
|
||||||
|
adapter := NewOpenAIAdapter()
|
||||||
|
adapter.client = server.Client()
|
||||||
|
err = adapter.Stream(ctx, req, sink)
|
||||||
|
} else {
|
||||||
|
adapter := NewAnthropicAdapter()
|
||||||
|
adapter.client = server.Client()
|
||||||
|
err = adapter.Stream(ctx, req, sink)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if text.String() != "ok" || !finished {
|
||||||
|
t.Fatalf("stream did not finish correctly: %q %t", text.String(), finished)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderDeadlineCancelsPendingHTTPResponse(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = io.Copy(io.Discard, r.Body); <-r.Context().Done() }))
|
||||||
|
defer server.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
_, err := DoProviderRequestWithRetry(ctx, server.Client(), "fixture", "", "", func(ctx context.Context) (*http.Request, error) {
|
||||||
|
return http.NewRequestWithContext(ctx, http.MethodPost, server.URL, strings.NewReader("fixture"))
|
||||||
|
})
|
||||||
|
if err == nil || ctx.Err() == nil {
|
||||||
|
t.Fatal("provider ignored cancellation")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,8 +85,12 @@ func ApplyCustomHeaders(httpReq *http.Request, enabled bool, headersJSON string)
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.EqualFold(name, "Host") {
|
||||||
|
httpReq.Host = value
|
||||||
|
} else {
|
||||||
httpReq.Header.Set(name, value)
|
httpReq.Header.Set(name, value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package modeladapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"cursor/internal/netproxy"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ func doProviderRequestWithRetry(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := netproxy.CredentialSafeClient(client).Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|||||||
@@ -746,7 +746,16 @@ func validateConversationID(conversationID string) (string, error) {
|
|||||||
if normalized == "" {
|
if normalized == "" {
|
||||||
return "", fmt.Errorf("conversation_id is required")
|
return "", fmt.Errorf("conversation_id is required")
|
||||||
}
|
}
|
||||||
if strings.Contains(normalized, "/") || strings.Contains(normalized, string(os.PathSeparator)) {
|
base := strings.ToUpper(strings.Split(normalized, ".")[0])
|
||||||
|
if base == "CON" || base == "PRN" || base == "AUX" || base == "NUL" || (len(base) == 4 && (strings.HasPrefix(base, "COM") || strings.HasPrefix(base, "LPT")) && base[3] >= '1' && base[3] <= '9') {
|
||||||
|
return "", fmt.Errorf("conversation_id is a reserved filename")
|
||||||
|
}
|
||||||
|
for _, r := range normalized {
|
||||||
|
if r < 32 || r == 127 {
|
||||||
|
return "", fmt.Errorf("invalid conversation_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normalized == "." || normalized == ".." || !filepath.IsLocal(normalized) || strings.ContainsAny(normalized, "/\\:\x00<>\"|?*") || strings.TrimRight(normalized, " .") != normalized {
|
||||||
return "", fmt.Errorf("conversation_id must not contain path separators")
|
return "", fmt.Errorf("conversation_id must not contain path separators")
|
||||||
}
|
}
|
||||||
return normalized, nil
|
return normalized, nil
|
||||||
|
|||||||
20
internal/backend/forwarder/file_store_security_test.go
Normal file
20
internal/backend/forwarder/file_store_security_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestConversationIDRejectsTraversalAndWindowsAliases(t *testing.T) {
|
||||||
|
for _, id := range []string{"", ".", "..", "../outside", `..\outside`, "C:evil", "CON", "id.", "id ", "a\x00b", "/absolute"} {
|
||||||
|
// Whitespace is trimmed for compatibility; the remaining alias must be safe.
|
||||||
|
if id == "id " {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := validateConversationID(id); err == nil {
|
||||||
|
t.Errorf("accepted %q", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range []string{"conversation-123", "a_b.c"} {
|
||||||
|
if _, err := validateConversationID(id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package forwarder
|
package forwarder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,7 +28,7 @@ func TestUpsertEventIdempotentBeyondRecentLimit(t *testing.T) {
|
|||||||
// 填充超过 RecentEvents 上限的无关事件,把目标事件挤出 RecentEvents。
|
// 填充超过 RecentEvents 上限的无关事件,把目标事件挤出 RecentEvents。
|
||||||
for i := range usageRecentEventLimit + 10 {
|
for i := range usageRecentEventLimit + 10 {
|
||||||
if err := store.UpsertEvent(usageFileEvent{
|
if err := store.UpsertEvent(usageFileEvent{
|
||||||
EventID: "filler",
|
EventID: fmt.Sprintf("filler-%d", i),
|
||||||
Kind: usageEventKindProvider,
|
Kind: usageEventKindProvider,
|
||||||
At: base.Add(time.Duration(i+1) * time.Second),
|
At: base.Add(time.Duration(i+1) * time.Second),
|
||||||
InputTokens: 1,
|
InputTokens: 1,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
serverconfig "cursor/internal/backend/server/config"
|
serverconfig "cursor/internal/backend/server/config"
|
||||||
"cursor/internal/backend/server/upstream"
|
"cursor/internal/backend/server/upstream"
|
||||||
"cursor/internal/certs"
|
"cursor/internal/certs"
|
||||||
|
"cursor/internal/localaccess"
|
||||||
"cursor/internal/logger"
|
"cursor/internal/logger"
|
||||||
"cursor/internal/netproxy"
|
"cursor/internal/netproxy"
|
||||||
legacyruntime "cursor/internal/runtime"
|
legacyruntime "cursor/internal/runtime"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
const healthPath = "/healthz"
|
const healthPath = "/healthz"
|
||||||
|
|
||||||
type Host struct {
|
type Host struct {
|
||||||
|
lifecycleMu sync.Mutex
|
||||||
store *serverconfig.Store
|
store *serverconfig.Store
|
||||||
listenAddr string
|
listenAddr string
|
||||||
cliHTTPSListenAddr string
|
cliHTTPSListenAddr string
|
||||||
@@ -86,6 +88,8 @@ func (host *Host) SaveConfig(ctx context.Context, cfg serverconfig.Config) (serv
|
|||||||
if host == nil || host.configs == nil {
|
if host == nil || host.configs == nil {
|
||||||
return serverconfig.Config{}, fmt.Errorf("backend config manager is not initialized")
|
return serverconfig.Config{}, fmt.Errorf("backend config manager is not initialized")
|
||||||
}
|
}
|
||||||
|
host.lifecycleMu.Lock()
|
||||||
|
defer host.lifecycleMu.Unlock()
|
||||||
normalized, err := host.configs.Save(ctx, cfg)
|
normalized, err := host.configs.Save(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return serverconfig.Config{}, err
|
return serverconfig.Config{}, err
|
||||||
@@ -157,6 +161,8 @@ func (host *Host) Start() error {
|
|||||||
if host == nil {
|
if host == nil {
|
||||||
return fmt.Errorf("backend host is nil")
|
return fmt.Errorf("backend host is nil")
|
||||||
}
|
}
|
||||||
|
host.lifecycleMu.Lock()
|
||||||
|
defer host.lifecycleMu.Unlock()
|
||||||
cfg := host.configs.Current()
|
cfg := host.configs.Current()
|
||||||
|
|
||||||
host.runMu.Lock()
|
host.runMu.Lock()
|
||||||
@@ -170,7 +176,7 @@ func (host *Host) Start() error {
|
|||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: host.listenAddr,
|
Addr: host.listenAddr,
|
||||||
Handler: host.mux,
|
Handler: localaccess.Wrap(host.mux),
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
listener, err := net.Listen("tcp", host.listenAddr)
|
listener, err := net.Listen("tcp", host.listenAddr)
|
||||||
@@ -199,8 +205,10 @@ func (host *Host) Start() error {
|
|||||||
|
|
||||||
// 启动 CLI HTTPS+HTTP/2 监听器
|
// 启动 CLI HTTPS+HTTP/2 监听器
|
||||||
if err := host.startHTTPSLocked(cfg); err != nil {
|
if err := host.startHTTPSLocked(cfg); err != nil {
|
||||||
// HTTPS 失败不阻断 HTTP,仅记录
|
host.httpServer = nil
|
||||||
logger.Errorf("启动 CLI HTTPS 监听器失败: %v", err)
|
_ = httpServer.Close()
|
||||||
|
host.lastRunErr = fmt.Errorf("启动 CLI HTTPS 监听器失败: %w", err)
|
||||||
|
return host.lastRunErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -230,7 +238,7 @@ func (host *Host) startHTTPSLocked(cfg serverconfig.Config) error {
|
|||||||
|
|
||||||
httpsServer := &http.Server{
|
httpsServer := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: host.mux,
|
Handler: localaccess.Wrap(host.mux),
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -262,28 +270,29 @@ func (host *Host) Stop(ctx context.Context) error {
|
|||||||
if host == nil {
|
if host == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
host.runMu.Lock()
|
host.lifecycleMu.Lock()
|
||||||
httpServer := host.httpServer
|
defer host.lifecycleMu.Unlock()
|
||||||
httpsServer := host.httpsServer
|
host.runMu.RLock()
|
||||||
host.httpServer = nil
|
httpServer, httpsServer := host.httpServer, host.httpsServer
|
||||||
host.httpsServer = nil
|
host.runMu.RUnlock()
|
||||||
host.runMu.Unlock()
|
|
||||||
var errs []error
|
var errs []error
|
||||||
if httpsServer != nil {
|
for _, srv := range []*http.Server{httpsServer, httpServer} {
|
||||||
if err := httpsServer.Shutdown(ctx); err != nil {
|
if srv != nil {
|
||||||
errs = append(errs, fmt.Errorf("stop CLI HTTPS: %w", err))
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
}
|
_ = srv.Close()
|
||||||
}
|
|
||||||
if serverInstance := httpServer; serverInstance != nil {
|
|
||||||
err := serverInstance.Shutdown(ctx)
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(errs) > 0 {
|
|
||||||
return errors.Join(errs...)
|
|
||||||
}
|
}
|
||||||
return nil
|
host.runMu.Lock()
|
||||||
|
if host.httpServer == httpServer {
|
||||||
|
host.httpServer = nil
|
||||||
|
}
|
||||||
|
if host.httpsServer == httpsServer {
|
||||||
|
host.httpsServer = nil
|
||||||
|
}
|
||||||
|
host.runMu.Unlock()
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (host *Host) HealthCheck(ctx context.Context) error {
|
func (host *Host) HealthCheck(ctx context.Context) error {
|
||||||
@@ -928,19 +937,27 @@ func uploadServiceProcedure(pattern string, name string, protocol server.RouteOp
|
|||||||
func tabServerUpstreamProcedure(pattern string, name string, protocol server.RouteOption, deps upstream.Dependencies, host *Host) server.Option {
|
func tabServerUpstreamProcedure(pattern string, name string, protocol server.RouteOption, deps upstream.Dependencies, host *Host) server.Option {
|
||||||
direct := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
|
direct := upstream.DirectAction(deps, upstream.CompatRouteConfig{Name: name})
|
||||||
action := func(ctx *server.Context) error {
|
action := func(ctx *server.Context) error {
|
||||||
if host == nil || host.tabServerBaseURL == "" {
|
if host == nil || ctx == nil || ctx.Request == nil {
|
||||||
|
return fmt.Errorf("invalid tab request")
|
||||||
|
}
|
||||||
|
cfg := host.configs.Current()
|
||||||
|
if cfg.TabServerBaseURL == "" {
|
||||||
http.NotFound(ctx.Writer, ctx.Request)
|
http.NotFound(ctx.Writer, ctx.Request)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil {
|
if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil {
|
||||||
baseURL, err := url.Parse(host.tabServerBaseURL)
|
baseURL, err := url.Parse(cfg.TabServerBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析 tab server 地址失败: %w", err)
|
return fmt.Errorf("解析 tab server 地址失败: %w", err)
|
||||||
}
|
}
|
||||||
targetURL := *ctx.Request.URL
|
targetURL := *ctx.Request.URL
|
||||||
targetURL.Scheme = baseURL.Scheme
|
targetURL.Scheme = baseURL.Scheme
|
||||||
targetURL.Host = baseURL.Host
|
targetURL.Host = baseURL.Host
|
||||||
|
targetURL.Path = strings.TrimRight(baseURL.Path, "/") + ctx.Request.URL.Path
|
||||||
|
targetURL.RawPath = ""
|
||||||
ctx.UpstreamURL = &targetURL
|
ctx.UpstreamURL = &targetURL
|
||||||
|
ctx.Request = ctx.Request.Clone(ctx.Request.Context())
|
||||||
|
ctx.Request.Header.Set("Authorization", "Bearer "+cfg.TabServerToken)
|
||||||
}
|
}
|
||||||
return direct(ctx)
|
return direct(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
105
internal/backend/host_lifecycle_test.go
Normal file
105
internal/backend/host_lifecycle_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
serverconfig "cursor/internal/backend/server/config"
|
||||||
|
"cursor/internal/certs"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func freeAddress(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
addr := l.Addr().String()
|
||||||
|
l.Close()
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPSBindFailureRollsBackHTTPListener(t *testing.T) {
|
||||||
|
occupied, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer occupied.Close()
|
||||||
|
cfg := serverconfig.DefaultConfig()
|
||||||
|
cfg.BackendListenAddr = freeAddress(t)
|
||||||
|
cfg.CLIHTTPSListenAddr = occupied.Addr().String()
|
||||||
|
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
|
||||||
|
if _, err = store.Save(nil, cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ca, err := certs.LoadOrCreateLocalCA(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
host, err := NewHost(store, ca)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = host.Start(); err == nil {
|
||||||
|
t.Fatal("HTTPS bind failure ignored")
|
||||||
|
}
|
||||||
|
if host.IsRunning() {
|
||||||
|
t.Fatal("partial service reported running")
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", cfg.BackendListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("HTTP listener leaked", err)
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShutdownTimeoutForceClosesConnectionAndReleasesPort(t *testing.T) {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
address := listener.Addr().String()
|
||||||
|
started := make(chan struct{})
|
||||||
|
ended := make(chan struct{})
|
||||||
|
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { close(started); <-r.Context().Done(); close(ended) })}
|
||||||
|
go server.Serve(listener)
|
||||||
|
host := &Host{httpServer: server}
|
||||||
|
requestDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(requestDone)
|
||||||
|
client := &http.Client{Timeout: time.Second}
|
||||||
|
resp, _ := client.Get("http://" + address)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-started:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("request not started")
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
err = host.Stop(ctx)
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Fatalf("expected timeout, got %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ended:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("handler still active")
|
||||||
|
}
|
||||||
|
<-requestDone
|
||||||
|
if host.IsRunning() {
|
||||||
|
t.Fatal("closed service still running")
|
||||||
|
}
|
||||||
|
rebound, err := net.Listen("tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("port not released", err)
|
||||||
|
}
|
||||||
|
rebound.Close()
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ func (manager *Manager) currentConfig() Config {
|
|||||||
return DefaultConfig()
|
return DefaultConfig()
|
||||||
}
|
}
|
||||||
if current := manager.current.Load(); current != nil {
|
if current := manager.current.Load(); current != nil {
|
||||||
return *current
|
return cloneConfig(*current)
|
||||||
}
|
}
|
||||||
return DefaultConfig()
|
return DefaultConfig()
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,12 @@ func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
|
|||||||
return Config{}, fmt.Errorf("config manager is not initialized")
|
return Config{}, fmt.Errorf("config manager is not initialized")
|
||||||
}
|
}
|
||||||
manager.saveMu.Lock()
|
manager.saveMu.Lock()
|
||||||
defer manager.saveMu.Unlock()
|
locked := true
|
||||||
|
defer func() {
|
||||||
|
if locked {
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
normalized, err := manager.store.Save(ctx, cfg)
|
normalized, err := manager.store.Save(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
@@ -86,6 +91,8 @@ func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
|
|||||||
manager.lastReload = time.Now()
|
manager.lastReload = time.Now()
|
||||||
manager.reloadError = ""
|
manager.reloadError = ""
|
||||||
manager.reloadMu.Unlock()
|
manager.reloadMu.Unlock()
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
locked = false
|
||||||
manager.notify(normalized)
|
manager.notify(normalized)
|
||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
@@ -104,13 +111,17 @@ func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string
|
|||||||
normalizedValue := strings.TrimSpace(value)
|
normalizedValue := strings.TrimSpace(value)
|
||||||
// 在 saveMu 保护下完成 read-modify-write,避免与并发 Save 互相覆盖。
|
// 在 saveMu 保护下完成 read-modify-write,避免与并发 Save 互相覆盖。
|
||||||
manager.saveMu.Lock()
|
manager.saveMu.Lock()
|
||||||
defer manager.saveMu.Unlock()
|
locked := true
|
||||||
|
defer func() {
|
||||||
|
if locked {
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
current := manager.currentConfig()
|
current := manager.currentConfig()
|
||||||
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
|
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
current.LastAgentModelHash = normalizedValue
|
normalized, err := manager.store.updateLastAgentModelHash(normalizedValue)
|
||||||
normalized, err := manager.store.Save(ctx, current)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -120,6 +131,8 @@ func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string
|
|||||||
manager.lastReload = time.Now()
|
manager.lastReload = time.Now()
|
||||||
manager.reloadError = ""
|
manager.reloadError = ""
|
||||||
manager.reloadMu.Unlock()
|
manager.reloadMu.Unlock()
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
locked = false
|
||||||
manager.notify(normalized)
|
manager.notify(normalized)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -165,6 +178,8 @@ func (manager *Manager) LegacyRuntimeSnapshot(_ context.Context) (legacyruntime.
|
|||||||
for _, item := range cfg.ModelAdapters {
|
for _, item := range cfg.ModelAdapters {
|
||||||
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
|
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
|
CustomHeadersEnabled: item.CustomHeadersEnabled, CustomHeadersJSON: item.CustomHeadersJSON,
|
||||||
|
AnthropicExtraParamsEnabled: item.AnthropicExtraParamsEnabled, AnthropicExtraParamsJSON: item.AnthropicExtraParamsJSON,
|
||||||
DisplayName: item.DisplayName,
|
DisplayName: item.DisplayName,
|
||||||
Type: item.Type,
|
Type: item.Type,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
@@ -204,7 +219,7 @@ func (manager *Manager) RouteMode(hasUpstreamURL bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) setCurrent(cfg Config) {
|
func (manager *Manager) setCurrent(cfg Config) {
|
||||||
next := cfg
|
next := cloneConfig(cfg)
|
||||||
manager.current.Store(&next)
|
manager.current.Store(&next)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +230,13 @@ func (manager *Manager) reloadIfChanged(ctx context.Context) {
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
manager.saveMu.Lock()
|
||||||
|
reloadSaveLocked := true
|
||||||
|
defer func() {
|
||||||
|
if reloadSaveLocked {
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
manager.reloadMu.Lock()
|
manager.reloadMu.Lock()
|
||||||
if !manager.lastReload.IsZero() && now.Sub(manager.lastReload) < configHotReloadMinInterval {
|
if !manager.lastReload.IsZero() && now.Sub(manager.lastReload) < configHotReloadMinInterval {
|
||||||
@@ -241,6 +263,8 @@ func (manager *Manager) reloadIfChanged(ctx context.Context) {
|
|||||||
manager.reloadError = ""
|
manager.reloadError = ""
|
||||||
manager.setCurrent(cfg)
|
manager.setCurrent(cfg)
|
||||||
manager.reloadMu.Unlock()
|
manager.reloadMu.Unlock()
|
||||||
|
manager.saveMu.Unlock()
|
||||||
|
reloadSaveLocked = false
|
||||||
manager.notify(cfg)
|
manager.notify(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +274,7 @@ func (manager *Manager) notify(cfg Config) {
|
|||||||
manager.listenersMu.RUnlock()
|
manager.listenersMu.RUnlock()
|
||||||
for _, listener := range listeners {
|
for _, listener := range listeners {
|
||||||
if listener != nil {
|
if listener != nil {
|
||||||
listener(cfg)
|
listener(cloneConfig(cfg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
internal/backend/server/config/revision.go
Normal file
33
internal/backend/server/config/revision.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrConfigConflict = errors.New("CONFIG_CONFLICT: 配置已在其他窗口更改,请重新加载后再保存;当前编辑内容已保留")
|
||||||
|
|
||||||
|
func cloneConfig(cfg Config) Config {
|
||||||
|
cfg.ModelAdapters = append([]ModelAdapterConfig(nil), cfg.ModelAdapters...)
|
||||||
|
if cfg.HomeMetrics.PersistentMode != nil {
|
||||||
|
value := *cfg.HomeMetrics.PersistentMode
|
||||||
|
cfg.HomeMetrics.PersistentMode = &value
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func withRevision(cfg Config) Config {
|
||||||
|
cfg = cloneConfig(cfg)
|
||||||
|
view := cloneConfig(cfg)
|
||||||
|
view.Revision = ""
|
||||||
|
view.LastAgentModelHash = ""
|
||||||
|
for i := range view.ModelAdapters {
|
||||||
|
view.ModelAdapters[i].ID = ""
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(view)
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
cfg.Revision = hex.EncodeToString(sum[:])
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
83
internal/backend/server/config/revision_test.go
Normal file
83
internal/backend/server/config/revision_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigCASAndMetadataIsolation(t *testing.T) {
|
||||||
|
store := NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
|
||||||
|
m, err := NewManager(context.Background(), store)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
a, _ := m.Load(nil)
|
||||||
|
b, _ := m.Load(nil)
|
||||||
|
if a.Revision == "" {
|
||||||
|
t.Fatal("missing revision")
|
||||||
|
}
|
||||||
|
if err = m.SaveLastAgentModelHash(nil, "model-new"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
a.CLIHTTPSListenAddr = "127.0.0.1:39099"
|
||||||
|
first, err := m.Save(nil, a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if first.LastAgentModelHash != "model-new" {
|
||||||
|
t.Fatal("UI overwrote backend metadata")
|
||||||
|
}
|
||||||
|
b.HomeMetrics.RefreshIntervalSeconds = 11
|
||||||
|
if _, err = m.Save(nil, b); !errors.Is(err, ErrConfigConflict) {
|
||||||
|
t.Fatalf("stale save accepted: %v", err)
|
||||||
|
}
|
||||||
|
current, _ := m.Load(nil)
|
||||||
|
if current.CLIHTTPSListenAddr != "127.0.0.1:39099" {
|
||||||
|
t.Fatal("CLI port lost")
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(store.Path())
|
||||||
|
if strings.Contains(string(data), "revision:") {
|
||||||
|
t.Fatal("revision persisted in YAML")
|
||||||
|
}
|
||||||
|
if current.Revision == a.Revision {
|
||||||
|
t.Fatal("revision did not change")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentSavesHaveExactlyOneWinner(t *testing.T) {
|
||||||
|
store := NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
|
||||||
|
m, err := NewManager(context.Background(), store)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
a, _ := m.Load(nil)
|
||||||
|
b := cloneConfig(a)
|
||||||
|
a.HomeMetrics.RefreshIntervalSeconds = 11
|
||||||
|
b.HomeMetrics.RefreshIntervalSeconds = 12
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make(chan error, 2)
|
||||||
|
for _, cfg := range []Config{a, b} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(c Config) { defer wg.Done(); _, err := m.Save(nil, c); results <- err }(cfg)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
success, conflict := 0, 0
|
||||||
|
for err := range results {
|
||||||
|
if err == nil {
|
||||||
|
success++
|
||||||
|
} else if errors.Is(err, ErrConfigConflict) {
|
||||||
|
conflict++
|
||||||
|
} else {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success != 1 || conflict != 1 {
|
||||||
|
t.Fatalf("success=%d conflict=%d", success, conflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"cursor/internal/securefile"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -75,7 +76,7 @@ func (store *Store) Load(_ context.Context) (Config, error) {
|
|||||||
if err := store.saveLocked(defaultConfig); err != nil {
|
if err := store.saveLocked(defaultConfig); err != nil {
|
||||||
return DefaultConfig(), err
|
return DefaultConfig(), err
|
||||||
}
|
}
|
||||||
return defaultConfig, nil
|
return withRevision(defaultConfig), nil
|
||||||
}
|
}
|
||||||
return DefaultConfig(), fmt.Errorf("读取用户配置失败: %w", err)
|
return DefaultConfig(), fmt.Errorf("读取用户配置失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ func (store *Store) Load(_ context.Context) (Config, error) {
|
|||||||
return DefaultConfig(), err
|
return DefaultConfig(), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return normalized, nil
|
return withRevision(normalized), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
|
func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
|
||||||
@@ -109,10 +110,20 @@ func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
|
|||||||
store.mu.Lock()
|
store.mu.Lock()
|
||||||
defer store.mu.Unlock()
|
defer store.mu.Unlock()
|
||||||
|
|
||||||
|
if cfg.Revision != "" {
|
||||||
|
existing, err := store.readCurrentLocked()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
if withRevision(existing).Revision != cfg.Revision {
|
||||||
|
return Config{}, ErrConfigConflict
|
||||||
|
}
|
||||||
|
normalized.LastAgentModelHash = existing.LastAgentModelHash
|
||||||
|
}
|
||||||
if err := store.saveLocked(normalized); err != nil {
|
if err := store.saveLocked(normalized); err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
return normalized, nil
|
return withRevision(normalized), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) saveLocked(normalized Config) error {
|
func (store *Store) saveLocked(normalized Config) error {
|
||||||
@@ -125,11 +136,7 @@ func (store *Store) saveLocked(normalized Config) error {
|
|||||||
return fmt.Errorf("序列化用户配置失败: %w", err)
|
return fmt.Errorf("序列化用户配置失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempPath := store.path + ".tmp"
|
if err := securefile.Write(store.path, data); err != nil {
|
||||||
if err := writeFileSync(tempPath, data); err != nil {
|
|
||||||
return fmt.Errorf("写入临时配置失败: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(tempPath, store.path); err != nil {
|
|
||||||
return fmt.Errorf("保存用户配置失败: %w", err)
|
return fmt.Errorf("保存用户配置失败: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -181,3 +188,31 @@ func yamlHasKey(raw []byte, key string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (store *Store) readCurrentLocked() (Config, error) {
|
||||||
|
data, err := os.ReadFile(store.path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return DefaultConfig(), nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
var current Config
|
||||||
|
if err = yaml.Unmarshal(data, ¤t); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
return NormalizeConfig(current)
|
||||||
|
}
|
||||||
|
func (store *Store) updateLastAgentModelHash(value string) (Config, error) {
|
||||||
|
store.mu.Lock()
|
||||||
|
defer store.mu.Unlock()
|
||||||
|
cfg, err := store.readCurrentLocked()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
cfg.LastAgentModelHash = value
|
||||||
|
if err = store.saveLocked(cfg); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
return withRevision(cfg), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cursor/internal/logger"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -62,6 +63,7 @@ type HomeMetricsConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Revision string `json:"revision" yaml:"-"`
|
||||||
Log bool `json:"log" yaml:"log"`
|
Log bool `json:"log" yaml:"log"`
|
||||||
ProviderStreamIdleTimeout int `json:"providerStreamIdleTimeout" yaml:"providerStreamIdleTimeout"`
|
ProviderStreamIdleTimeout int `json:"providerStreamIdleTimeout" yaml:"providerStreamIdleTimeout"`
|
||||||
BackendListenAddr string `json:"backendListenAddr" yaml:"backendListenAddr"`
|
BackendListenAddr string `json:"backendListenAddr" yaml:"backendListenAddr"`
|
||||||
@@ -139,6 +141,13 @@ func NormalizeConfig(input Config) (Config, error) {
|
|||||||
}
|
}
|
||||||
output.TabServerBaseURL = normalizeTabServerURL(input.TabServerBaseURL)
|
output.TabServerBaseURL = normalizeTabServerURL(input.TabServerBaseURL)
|
||||||
output.TabServerToken = strings.TrimSpace(input.TabServerToken)
|
output.TabServerToken = strings.TrimSpace(input.TabServerToken)
|
||||||
|
if strings.TrimSpace(input.TabServerBaseURL) != "" && output.TabServerBaseURL == "" {
|
||||||
|
return Config{}, fmt.Errorf("tabServerBaseURL 无效")
|
||||||
|
}
|
||||||
|
if output.TabServerBaseURL != "" && output.TabServerToken == "" {
|
||||||
|
return Config{}, fmt.Errorf("配置 Tab 服务时必须提供独立 tabServerToken")
|
||||||
|
}
|
||||||
|
logger.RegisterSecrets(output.TabServerToken)
|
||||||
adapters, err := NormalizeModelAdapterConfigs(input.ModelAdapters)
|
adapters, err := NormalizeModelAdapterConfigs(input.ModelAdapters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
@@ -156,7 +165,7 @@ func normalizeTabServerURL(value string) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
if parsed.User != nil || parsed.Hostname() == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
||||||
@@ -193,6 +202,7 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
|
|||||||
AnthropicMaxTokens: normalizeMaxCompletionTokens(item.AnthropicMaxTokens),
|
AnthropicMaxTokens: normalizeMaxCompletionTokens(item.AnthropicMaxTokens),
|
||||||
ThinkingBudgetTokens: normalizeMaxCompletionTokens(item.ThinkingBudgetTokens),
|
ThinkingBudgetTokens: normalizeMaxCompletionTokens(item.ThinkingBudgetTokens),
|
||||||
}
|
}
|
||||||
|
logger.RegisterSecrets(next.APIKey)
|
||||||
if next.Type == "openai" {
|
if next.Type == "openai" {
|
||||||
next.OpenAIExtraParamsEnabled = item.OpenAIExtraParamsEnabled
|
next.OpenAIExtraParamsEnabled = item.OpenAIExtraParamsEnabled
|
||||||
next.OpenAIExtraParamsJSON = strings.TrimSpace(item.OpenAIExtraParamsJSON)
|
next.OpenAIExtraParamsJSON = strings.TrimSpace(item.OpenAIExtraParamsJSON)
|
||||||
@@ -203,6 +213,15 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
|
|||||||
}
|
}
|
||||||
next.CustomHeadersEnabled = item.CustomHeadersEnabled
|
next.CustomHeadersEnabled = item.CustomHeadersEnabled
|
||||||
next.CustomHeadersJSON = strings.TrimSpace(item.CustomHeadersJSON)
|
next.CustomHeadersJSON = strings.TrimSpace(item.CustomHeadersJSON)
|
||||||
|
var headers map[string]string
|
||||||
|
if json.Unmarshal([]byte(next.CustomHeadersJSON), &headers) == nil {
|
||||||
|
for key, value := range headers {
|
||||||
|
lower := strings.ToLower(key)
|
||||||
|
if strings.Contains(lower, "auth") || strings.Contains(lower, "key") || strings.Contains(lower, "token") || strings.Contains(lower, "cookie") {
|
||||||
|
logger.RegisterSecrets(value, strings.TrimPrefix(value, "Bearer "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case next.DisplayName == "":
|
case next.DisplayName == "":
|
||||||
return nil, errors.New("模型适配器 displayName 不能为空")
|
return nil, errors.New("模型适配器 displayName 不能为空")
|
||||||
|
|||||||
65
internal/backend/tab_forward_test.go
Normal file
65
internal/backend/tab_forward_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"cursor/internal/backend/server"
|
||||||
|
serverconfig "cursor/internal/backend/server/config"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTabCallerTokenAndBasePathFollowCurrentConfiguration(t *testing.T) {
|
||||||
|
expected := "client-first"
|
||||||
|
calls := 0
|
||||||
|
tab := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls++
|
||||||
|
if r.Header.Get("Authorization") != "Bearer "+expected {
|
||||||
|
t.Error("configured caller token not forwarded")
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/base/aiserver.v1.AiService/CppConfig" {
|
||||||
|
t.Error("base path lost", r.URL.Path)
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}))
|
||||||
|
defer tab.Close()
|
||||||
|
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
|
||||||
|
cfg := serverconfig.DefaultConfig()
|
||||||
|
cfg.TabServerBaseURL = tab.URL + "/base"
|
||||||
|
cfg.TabServerToken = expected
|
||||||
|
if _, err := store.Save(nil, cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
host, err := NewHost(store, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
local := httptest.NewServer(host.mux)
|
||||||
|
defer local.Close()
|
||||||
|
call := func() {
|
||||||
|
r, _ := http.NewRequest(http.MethodPost, local.URL+"/aiserver.v1.AiService/CppConfig", bytes.NewReader(nil))
|
||||||
|
r.Header.Set("Content-Type", "application/proto")
|
||||||
|
r.Header.Set(server.HeaderServerUpstreamURL, "https://api2.cursor.sh/aiserver.v1.AiService/CppConfig")
|
||||||
|
r.Header.Set("Authorization", "Bearer unrelated-cursor-token")
|
||||||
|
response, err := local.Client().Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
if response.StatusCode != 204 {
|
||||||
|
t.Fatal(response.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call()
|
||||||
|
cfg = host.ConfigManager().Current()
|
||||||
|
expected = "client-second"
|
||||||
|
cfg.TabServerToken = expected
|
||||||
|
if _, err = host.SaveConfig(nil, cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
call()
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatal("Tab requests did not reach configured server")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,11 +25,6 @@ import (
|
|||||||
//go:embed ca.crt
|
//go:embed ca.crt
|
||||||
var embeddedCACertPEM []byte
|
var embeddedCACertPEM []byte
|
||||||
|
|
||||||
// embeddedCAKeyPEM 表示当前模块中的 embeddedCAKeyPEM 状态值。
|
|
||||||
//
|
|
||||||
//go:embed ca.key
|
|
||||||
var embeddedCAKeyPEM []byte
|
|
||||||
|
|
||||||
// Manager 定义了当前模块中的 Manager 类型。
|
// Manager 定义了当前模块中的 Manager 类型。
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
// caCert 表示当前声明中的 caCert。
|
// caCert 表示当前声明中的 caCert。
|
||||||
@@ -52,27 +47,23 @@ func NewManager(caCertPath, caKeyPath string) (*Manager, error) {
|
|||||||
return NewManagerFromPEM(certPEM, keyPEM)
|
return NewManagerFromPEM(certPEM, keyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEmbeddedManager 用于处理与 NewEmbeddedManager 相关的逻辑。
|
|
||||||
func NewEmbeddedManager() (*Manager, error) {
|
|
||||||
return NewManagerFromPEM(embeddedCACertPEM, embeddedCAKeyPEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmbeddedCACertPEM 用于处理与 EmbeddedCACertPEM 相关的逻辑。
|
// EmbeddedCACertPEM 用于处理与 EmbeddedCACertPEM 相关的逻辑。
|
||||||
func EmbeddedCACertPEM() []byte {
|
func EmbeddedCACertPEM() []byte {
|
||||||
return cloneBytes(embeddedCACertPEM)
|
return cloneBytes(embeddedCACertPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmbeddedCAKeyPEM 用于处理与 EmbeddedCAKeyPEM 相关的逻辑。
|
|
||||||
func EmbeddedCAKeyPEM() []byte {
|
|
||||||
return cloneBytes(embeddedCAKeyPEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManagerFromPEM 用于处理与 NewManagerFromPEM 相关的逻辑。
|
// NewManagerFromPEM 用于处理与 NewManagerFromPEM 相关的逻辑。
|
||||||
func NewManagerFromPEM(caCertPEM, caKeyPEM []byte) (*Manager, error) {
|
func NewManagerFromPEM(caCertPEM, caKeyPEM []byte) (*Manager, error) {
|
||||||
|
if _, err := tls.X509KeyPair(caCertPEM, caKeyPEM); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
caCert, caKey, err := loadCAFromPEM(caCertPEM, caKeyPEM)
|
caCert, caKey, err := loadCAFromPEM(caCertPEM, caKeyPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !caCert.IsCA || caCert.KeyUsage&x509.KeyUsageCertSign == 0 || time.Now().Before(caCert.NotBefore) || !time.Now().Before(caCert.NotAfter) {
|
||||||
|
return nil, errors.New("invalid or expired CA certificate")
|
||||||
|
}
|
||||||
return &Manager{caCert: caCert, caKey: caKey, cache: make(map[string]*tls.Certificate)}, nil
|
return &Manager{caCert: caCert, caKey: caKey, cache: make(map[string]*tls.Certificate)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +89,7 @@ func (m *Manager) CertificateForServerName(serverName string) (*tls.Certificate,
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if cert, ok := m.cache[host]; ok {
|
if cert, ok := m.cache[host]; ok && cert.Leaf != nil && time.Now().Add(time.Minute).Before(cert.Leaf.NotAfter) {
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +110,12 @@ func (m *Manager) CertificateForServerName(serverName string) (*tls.Certificate,
|
|||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
}
|
}
|
||||||
|
if !time.Now().Before(m.caCert.NotAfter) {
|
||||||
|
return nil, errors.New("CA certificate expired")
|
||||||
|
}
|
||||||
|
if leaf.NotAfter.After(m.caCert.NotAfter) {
|
||||||
|
leaf.NotAfter = m.caCert.NotAfter
|
||||||
|
}
|
||||||
if len(m.caCert.SubjectKeyId) > 0 {
|
if len(m.caCert.SubjectKeyId) > 0 {
|
||||||
leaf.AuthorityKeyId = append([]byte(nil), m.caCert.SubjectKeyId...)
|
leaf.AuthorityKeyId = append([]byte(nil), m.caCert.SubjectKeyId...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEpAIBAAKCAQEAyh3jND/aFusuRjGTmhQtX2hkF1qroNjEEKCxWPlfprvdl8Tx
|
|
||||||
upxNv1TQcm+K9KsS7OxnKYP4Qtv068XLbaCCGuoA/xpor6enrT85KulBq0j8z/g1
|
|
||||||
y0VWxjz3xN9F9ND13h9yxDZCn76egbRkhFxpXow67jLsIlrqWDSltERlTKh2cJ1g
|
|
||||||
hTRuQNSr7jtKQgFAR3aQ6dTzOP4fCOtLYn63jL4+YcxdGoK66tx8eFHq8oBLYvsc
|
|
||||||
LNjjkaAHilZFwA4Jr7zHBofTRBg04eZum2UTaRqmSyT65ifXm4vRdQa4k1FS1gnq
|
|
||||||
hPMhIjST8b6RkxvbLNmFpkOfI9eCMRpFG7y+NQIDAQABAoIBAATU9ZVOcHmLSkop
|
|
||||||
zcBJerM09O2dAIziGb/XA55fqdJ728aQ0gGW0oIANlKCCaWjQFrTJP04VzNL/F01
|
|
||||||
l5EpnOqlTPxMRpPqc2cAI677sBL29fpH0gtnvzUSiI7Xkp3RcAtNH6qCrJGSlkn+
|
|
||||||
BMgoSGmW+yKuK3h/yWnt6kc2umA8fN+bHKhS3pI56PMW8qVnny9n92RaCA7Uf/4j
|
|
||||||
XDewIreiH5jRqRwrPbOpjDFmv+W18LWZQiTwwxfY6sRZiZpsfsHidzfFGUFZXMlq
|
|
||||||
2P3FCqoF4oMM1rRgBlHhDR7JHmSkFpZG639HJTXLllpyDbj3H86nsjIj9WZYKn+h
|
|
||||||
B9k9bcECgYEA1HDzRCqoRZh6cL46KiYjv+LwmEKMa3nPb4ljywgLbRkzTMyVs0MK
|
|
||||||
fDsDoTLFY9PBhveypU5gjTbQqtyBsDtbU+dH9Eks1FdL9P8bJf0ZOJFRiEB7uB4a
|
|
||||||
z3V9tcXHwH4l2bCWbWThQGFwRudDAoY0EH89oSA/WHayjOKe1Wi4ovUCgYEA848B
|
|
||||||
cYi+Qbkk+fOv9gSJn8KS1LH/jE28S/e7E4YkTfYUuu+7wr8bdRKUNpPIsLX0Fo9R
|
|
||||||
KpJX0Oyjjady9n/8ARZRmD8Upl+F7Sl7Ro6F8+nfqQUrxbVDiIL3b+aZF+cDfFrB
|
|
||||||
/xL5kyZZqTtFfP360tfYnlS6Sssd4E2Jsj0fJkECgYEA0gG+WbqZkgMDtwQ114jQ
|
|
||||||
elZLZRkUWwKVjzsQDZssQHNTBS6RJh619M0Z73aTLvYcL+IZFdT/GVoAuYc2JRLo
|
|
||||||
W28c8F6OFHMfwVeWbN1g20y8fqbQJtiLxF3vIYwcxStvG123tvisu8oXBeCDm7Ez
|
|
||||||
MsO2FtwcAsWECEXWojzdmSkCgYEA18GvPawtHnuszd+Z2Q5b/DKZb+Hex6N1Ura5
|
|
||||||
+qmyL333D0Kfyf0RjbxPn6l690+4UuPSuyu4r1Nx72KO7N6jlzL2RTBcUqX8NgOx
|
|
||||||
OOe4skJT5561EAdrM9sQ5wgYRpxW8ipUAGoGvNwUQV5ISFmVgIHFWz0jam5UoQcP
|
|
||||||
G94ZYgECgYBN4PB8BuAwMRqhTCLL5RJKtcdC/Ls9xdWrmswtQQ4OSBUTLGdeMhEN
|
|
||||||
E23F0d+NdtiTWDYRDJ7z6KAF8CcOwWMt2sNbrxgNRuDzyfLqbuVLOM8iY3xA50Nl
|
|
||||||
clZWujKILWp47+gA6/AqJLv2lA2LK8gM8zYzzm/nCuLCBLYH/hOiCA==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
78
internal/certs/local.go
Normal file
78
internal/certs/local.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cursor/internal/securefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadOrCreateLocalCA persists one private CA per application data directory.
|
||||||
|
// A single private bundle is the authority; ca.crt is a public export for clients.
|
||||||
|
func LoadOrCreateLocalCA(dir string) (*Manager, error) {
|
||||||
|
if err := securefile.PrivateDir(dir); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
unlock, err := securefile.Lock(filepath.Join(dir, "ca.lock"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
bundlePath := filepath.Join(dir, "local-ca.pem")
|
||||||
|
bundle, err := os.ReadFile(bundlePath)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
key, keyErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if keyErr != nil {
|
||||||
|
return nil, keyErr
|
||||||
|
}
|
||||||
|
serial, keyErr := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if keyErr != nil {
|
||||||
|
return nil, keyErr
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
template := &x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: "Local-Cursor Private CA", Organization: []string{"Local-Cursor"}}, NotBefore: now.Add(-time.Hour), NotAfter: now.AddDate(10, 0, 0), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign}
|
||||||
|
der, keyErr := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||||
|
if keyErr != nil {
|
||||||
|
return nil, keyErr
|
||||||
|
}
|
||||||
|
keyPEM, keyErr := marshalPrivateKeyPEM(key)
|
||||||
|
if keyErr != nil {
|
||||||
|
return nil, keyErr
|
||||||
|
}
|
||||||
|
bundle = append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), keyPEM...)
|
||||||
|
if err = securefile.Write(bundlePath, bundle); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = securefile.Protect(bundlePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certBlock, rest := pem.Decode(bundle)
|
||||||
|
if certBlock == nil {
|
||||||
|
return nil, fmt.Errorf("本机 CA 文件损坏,请先备份并修复")
|
||||||
|
}
|
||||||
|
manager, err := NewManagerFromPEM(pem.EncodeToMemory(certBlock), rest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("本机 CA 校验失败(不会自动轮换): %w", err)
|
||||||
|
}
|
||||||
|
if err = securefile.Write(filepath.Join(dir, "ca.crt"), manager.CACertPEM()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CACertPEM() []byte {
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: m.caCert.Raw})
|
||||||
|
}
|
||||||
70
internal/certs/local_test.go
Normal file
70
internal/certs/local_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/pem"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocalCAStablePrivateAndIndependent(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
a, err := LoadOrCreateLocalCA(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, err := LoadOrCreateLocalCA(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(a.CACertPEM(), b.CACertPEM()) {
|
||||||
|
t.Fatal("CA rotated on restart")
|
||||||
|
}
|
||||||
|
c, err := LoadOrCreateLocalCA(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if bytes.Equal(a.CACertPEM(), c.CACertPEM()) {
|
||||||
|
t.Fatal("installations share a CA")
|
||||||
|
}
|
||||||
|
if bytes.Equal(a.CACertPEM(), EmbeddedCACertPEM()) {
|
||||||
|
t.Fatal("legacy CA reused")
|
||||||
|
}
|
||||||
|
leaf, err := a.CertificateForServerName("api2.cursor.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := leaf.Leaf.CheckSignatureFrom(a.caCert); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := leaf.Leaf.VerifyHostname("api2.cursor.sh"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := leaf.Leaf.VerifyHostname("unrelated.example"); err == nil {
|
||||||
|
t.Fatal("hostname mismatch accepted")
|
||||||
|
}
|
||||||
|
bundle, err := os.ReadFile(filepath.Join(dir, "local-ca.pem"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, key := pem.Decode(bundle)
|
||||||
|
if _, err = NewManagerFromPEM(c.CACertPEM(), key); err == nil {
|
||||||
|
t.Fatal("mismatched key accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorruptCADoesNotRotateOrOverwrite(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
bad := []byte("broken private authority")
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "local-ca.pem"), bad, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := LoadOrCreateLocalCA(dir); err == nil {
|
||||||
|
t.Fatal("corrupt CA accepted")
|
||||||
|
}
|
||||||
|
after, _ := os.ReadFile(filepath.Join(dir, "local-ca.pem"))
|
||||||
|
if !bytes.Equal(bad, after) {
|
||||||
|
t.Fatal("corrupt CA silently replaced")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"cursor/internal/appdata"
|
"cursor/internal/appdata"
|
||||||
serverconfig "cursor/internal/backend/server/config"
|
serverconfig "cursor/internal/backend/server/config"
|
||||||
@@ -22,8 +23,8 @@ func (s *ProxyService) LoadUserConfig() (UserConfig, error) {
|
|||||||
if app != nil {
|
if app != nil {
|
||||||
ctx = app.Context()
|
ctx = app.Context()
|
||||||
}
|
}
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
return s.backendHost.LoadConfig(ctx)
|
return s.backend().LoadConfig(ctx)
|
||||||
}
|
}
|
||||||
if s.store == nil {
|
if s.store == nil {
|
||||||
return serverconfig.DefaultConfig(), nil
|
return serverconfig.DefaultConfig(), nil
|
||||||
@@ -36,6 +37,9 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
|
|||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if cfg.Revision == "" {
|
||||||
|
return fmt.Errorf("配置缺少 revision,请重新加载后保存")
|
||||||
|
}
|
||||||
app := application.Get()
|
app := application.Get()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if app != nil {
|
if app != nil {
|
||||||
@@ -45,8 +49,8 @@ func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
|
|||||||
normalized UserConfig
|
normalized UserConfig
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
normalized, err = s.backendHost.SaveConfig(ctx, cfg)
|
normalized, err = s.backend().SaveConfig(ctx, cfg)
|
||||||
} else if s.store != nil {
|
} else if s.store != nil {
|
||||||
normalized, err = s.store.Save(ctx, cfg)
|
normalized, err = s.store.Save(ctx, cfg)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
goruntime "runtime"
|
goruntime "runtime"
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
|
|
||||||
// ApplyCursorSettings 用于处理与 ApplyCursorSettings 相关的逻辑。
|
// ApplyCursorSettings 用于处理与 ApplyCursorSettings 相关的逻辑。
|
||||||
func (s *ProxyService) ApplyCursorSettings() error {
|
func (s *ProxyService) ApplyCursorSettings() error {
|
||||||
if s == nil || s.proxy == nil {
|
if s == nil || s.proxyServer() == nil {
|
||||||
return fmt.Errorf("proxy is not initialized")
|
return fmt.Errorf("proxy is not initialized")
|
||||||
}
|
}
|
||||||
s.caFileMu.Lock()
|
s.caFileMu.Lock()
|
||||||
@@ -37,7 +38,7 @@ func (s *ProxyService) ApplyCursorSettings() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyURL := cursor.ProxyURLFromListenAddr(s.proxy.Snapshot().ListenAddr)
|
proxyURL := cursor.ProxyURLFromListenAddr(s.proxyServer().Snapshot().ListenAddr)
|
||||||
|
|
||||||
if err := cursor.WriteUserProxySettings(proxyURL); err != nil {
|
if err := cursor.WriteUserProxySettings(proxyURL); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -48,8 +49,8 @@ func (s *ProxyService) ApplyCursorSettings() error {
|
|||||||
// 如果未检测到 CLI 安装,跳过但不报错
|
// 如果未检测到 CLI 安装,跳过但不报错
|
||||||
if cliPath := cursor.DetectCLIInstallPath(); cliPath != "" {
|
if cliPath := cursor.DetectCLIInstallPath(); cliPath != "" {
|
||||||
cliHTTPSAddr := ""
|
cliHTTPSAddr := ""
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
cliHTTPSAddr = s.backendHost.CLIHTTPSListenAddr()
|
cliHTTPSAddr = s.backend().CLIHTTPSListenAddr()
|
||||||
}
|
}
|
||||||
if err := cursor.WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath); err != nil {
|
if err := cursor.WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath); err != nil {
|
||||||
logger.Errorf("write cli proxy wrapper failed: %v", err)
|
logger.Errorf("write cli proxy wrapper failed: %v", err)
|
||||||
@@ -68,22 +69,21 @@ func (s *ProxyService) ApplyCursorSettings() error {
|
|||||||
|
|
||||||
// ClearCursorSettings 用于处理与 ClearCursorSettings 相关的逻辑。
|
// ClearCursorSettings 用于处理与 ClearCursorSettings 相关的逻辑。
|
||||||
func (s *ProxyService) ClearCursorSettings() error {
|
func (s *ProxyService) ClearCursorSettings() error {
|
||||||
|
var errs []error
|
||||||
if goruntime.GOOS == "darwin" {
|
if goruntime.GOOS == "darwin" {
|
||||||
if err := cursor.ClearSystemNodeExtraCACerts(); err != nil {
|
if err := cursor.ClearSystemNodeExtraCACerts(); err != nil {
|
||||||
return err
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := cursor.ClearUserProxySettings(); err != nil {
|
for _, clear := range []func() error{cursor.ClearUserProxySettings, cursor.ClearCLIProxyWrapper, cursor.ClearCLIAuthFile} {
|
||||||
return err
|
if err := clear(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
if err := cursor.ClearCLIProxyWrapper(); err != nil {
|
|
||||||
logger.Errorf("clear cli proxy wrapper failed: %v", err)
|
|
||||||
}
|
|
||||||
if err := cursor.ClearCLIAuthFile(); err != nil {
|
|
||||||
logger.Errorf("clear cli auth.json failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
s.setCursorSettingsApplied(false)
|
s.setCursorSettingsApplied(false)
|
||||||
return nil
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
||||||
|
|||||||
@@ -58,8 +58,34 @@ type ProxyState struct {
|
|||||||
|
|
||||||
// StartProxy 用于处理与 StartProxy 相关的逻辑。
|
// StartProxy 用于处理与 StartProxy 相关的逻辑。
|
||||||
func (s *ProxyService) StartProxy() (ProxyState, error) {
|
func (s *ProxyService) StartProxy() (ProxyState, error) {
|
||||||
|
if s == nil {
|
||||||
|
return ProxyState{}, fmt.Errorf("服务未初始化")
|
||||||
|
}
|
||||||
|
s.lifecycleMu.Lock()
|
||||||
|
defer s.lifecycleMu.Unlock()
|
||||||
|
if s.backend() != nil && s.backend().IsRunning() && s.proxyServer() != nil && s.proxyServer().IsRunning() {
|
||||||
|
return s.GetState(), nil
|
||||||
|
}
|
||||||
|
startedBackend, startedProxy, settingsAttempted := false, false, false
|
||||||
|
rollback := func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if startedProxy && s.proxyServer() != nil {
|
||||||
|
_ = s.proxyServer().Stop(ctx)
|
||||||
|
}
|
||||||
|
if settingsAttempted {
|
||||||
|
if err := s.ClearCursorSettings(); err != nil {
|
||||||
|
logger.Error("startup rollback failed", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if startedBackend && s.backend() != nil {
|
||||||
|
_ = s.backend().Stop(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("start service requested config_path=%s logs_root=%s", s.configPath, s.logsRoot)
|
logger.Infof("start service requested config_path=%s logs_root=%s", s.configPath, s.logsRoot)
|
||||||
fail := func(step string, err error) (ProxyState, error) {
|
fail := func(step string, err error) (ProxyState, error) {
|
||||||
|
rollback()
|
||||||
logger.Errorf("start service failed step=%s err=%v", step, err)
|
logger.Errorf("start service failed step=%s err=%v", step, err)
|
||||||
s.setLastError(err)
|
s.setLastError(err)
|
||||||
s.emitState()
|
s.emitState()
|
||||||
@@ -72,51 +98,43 @@ func (s *ProxyService) StartProxy() (ProxyState, error) {
|
|||||||
if err := s.ensureBackendHost(); err != nil {
|
if err := s.ensureBackendHost(); err != nil {
|
||||||
return fail("ensure_backend_host", err)
|
return fail("ensure_backend_host", err)
|
||||||
}
|
}
|
||||||
if !s.backendHost.IsRunning() {
|
if !s.backend().IsRunning() {
|
||||||
logger.Infof("starting embedded backend listen_addr=%s", s.backendHost.ListenAddr())
|
logger.Infof("starting embedded backend listen_addr=%s", s.backend().ListenAddr())
|
||||||
if err := s.backendHost.Start(); err != nil {
|
if err := s.backend().Start(); err != nil {
|
||||||
return fail("start_backend", err)
|
return fail("start_backend", err)
|
||||||
}
|
}
|
||||||
|
startedBackend = true
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("embedded backend already running listen_addr=%s", s.backendHost.ListenAddr())
|
logger.Infof("embedded backend already running listen_addr=%s", s.backend().ListenAddr())
|
||||||
}
|
}
|
||||||
healthCtx, healthCancel := context.WithTimeout(context.Background(), backendReadyTimeout)
|
healthCtx, healthCancel := context.WithTimeout(context.Background(), backendReadyTimeout)
|
||||||
defer healthCancel()
|
defer healthCancel()
|
||||||
if err := s.waitForBackend(healthCtx); err != nil {
|
if err := s.waitForBackend(healthCtx); err != nil {
|
||||||
return fail("wait_backend_ready", err)
|
return fail("wait_backend_ready", err)
|
||||||
}
|
}
|
||||||
logger.Infof("embedded backend ready listen_addr=%s", s.backendHost.ListenAddr())
|
logger.Infof("embedded backend ready listen_addr=%s", s.backend().ListenAddr())
|
||||||
if err := s.ensureProxy(cfg); err != nil {
|
if err := s.ensureProxy(cfg); err != nil {
|
||||||
return fail("ensure_proxy", err)
|
return fail("ensure_proxy", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.proxyServer() != nil && !s.proxyServer().IsRunning() {
|
||||||
|
logger.Infof("starting mitm proxy listen_addr=%s", s.proxyServer().Snapshot().ListenAddr)
|
||||||
|
if err := s.proxyServer().Start(); err != nil {
|
||||||
|
return fail("start_mitm_proxy", err)
|
||||||
|
}
|
||||||
|
startedProxy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsAttempted = true
|
||||||
|
if err := s.ApplyCursorSettings(); err != nil {
|
||||||
|
return fail("apply_cursor_settings", err)
|
||||||
|
}
|
||||||
// 启动时注入账号信息
|
// 启动时注入账号信息
|
||||||
if err := cursor.InjectCursorUserInfo(localruntime.InjectAccountEmail, localruntime.InjectAuthToken); err != nil {
|
if err := cursor.InjectCursorUserInfo(localruntime.InjectAccountEmail, localruntime.InjectAuthToken); err != nil {
|
||||||
logger.Errorf("injectCursorUserInfo failed: %v", err)
|
logger.Errorf("injectCursorUserInfo failed: %v", err)
|
||||||
// 不阻断启动,仅记录日志
|
// 不阻断启动,仅记录日志
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.proxy != nil && !s.proxy.IsRunning() {
|
|
||||||
logger.Infof("starting mitm proxy listen_addr=%s", s.proxy.Snapshot().ListenAddr)
|
|
||||||
if err := s.proxy.Start(); err != nil {
|
|
||||||
return fail("start_mitm_proxy", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.ApplyCursorSettings(); err != nil {
|
|
||||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer stopCancel()
|
|
||||||
if s.proxy != nil {
|
|
||||||
_ = s.proxy.Stop(stopCtx)
|
|
||||||
}
|
|
||||||
_ = s.backendHost.Stop(stopCtx)
|
|
||||||
startErr := fmt.Errorf("服务已启动,但注入 Cursor 配置失败: %w", err)
|
|
||||||
logger.Errorf("start service failed step=apply_cursor_settings err=%v", startErr)
|
|
||||||
s.setLastError(startErr)
|
|
||||||
s.emitState()
|
|
||||||
return s.GetState(), startErr
|
|
||||||
}
|
|
||||||
|
|
||||||
s.setLastError(nil)
|
s.setLastError(nil)
|
||||||
s.emitState()
|
s.emitState()
|
||||||
state := s.GetState()
|
state := s.GetState()
|
||||||
@@ -133,6 +151,11 @@ func (s *ProxyService) StartProxy() (ProxyState, error) {
|
|||||||
// 各清理步骤失败时记录错误但继续执行后续步骤,保证 backend 最终被停止,
|
// 各清理步骤失败时记录错误但继续执行后续步骤,保证 backend 最终被停止,
|
||||||
// 避免"Cursor 配置已清理但 backend 仍在运行"的不一致状态。
|
// 避免"Cursor 配置已清理但 backend 仍在运行"的不一致状态。
|
||||||
func (s *ProxyService) StopProxy() (ProxyState, error) {
|
func (s *ProxyService) StopProxy() (ProxyState, error) {
|
||||||
|
if s == nil {
|
||||||
|
return ProxyState{}, nil
|
||||||
|
}
|
||||||
|
s.lifecycleMu.Lock()
|
||||||
|
defer s.lifecycleMu.Unlock()
|
||||||
logger.Infof("stop service requested")
|
logger.Infof("stop service requested")
|
||||||
var stopErrors []error
|
var stopErrors []error
|
||||||
fail := func(step string, err error) {
|
fail := func(step string, err error) {
|
||||||
@@ -144,15 +167,15 @@ func (s *ProxyService) StopProxy() (ProxyState, error) {
|
|||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if s.proxy != nil && s.proxy.IsRunning() {
|
if s.proxyServer() != nil && s.proxyServer().IsRunning() {
|
||||||
logger.Infof("stopping mitm proxy listen_addr=%s", s.proxy.Snapshot().ListenAddr)
|
logger.Infof("stopping mitm proxy listen_addr=%s", s.proxyServer().Snapshot().ListenAddr)
|
||||||
fail("stop_mitm_proxy", s.proxy.Stop(ctx))
|
fail("stop_mitm_proxy", s.proxyServer().Stop(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fail("clear_cursor_settings", s.ClearCursorSettings())
|
fail("clear_cursor_settings", s.ClearCursorSettings())
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
logger.Infof("stopping embedded backend listen_addr=%s", s.backendHost.ListenAddr())
|
logger.Infof("stopping embedded backend listen_addr=%s", s.backend().ListenAddr())
|
||||||
fail("stop_backend", s.backendHost.Stop(ctx))
|
fail("stop_backend", s.backend().Stop(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stopErrors) > 0 {
|
if len(stopErrors) > 0 {
|
||||||
@@ -177,8 +200,8 @@ func (s *ProxyService) StopProxy() (ProxyState, error) {
|
|||||||
// GetState 用于处理与 GetState 相关的逻辑。
|
// GetState 用于处理与 GetState 相关的逻辑。
|
||||||
func (s *ProxyService) GetState() ProxyState {
|
func (s *ProxyService) GetState() ProxyState {
|
||||||
var proxySnap mitm.Snapshot
|
var proxySnap mitm.Snapshot
|
||||||
if s.proxy != nil {
|
if s.proxyServer() != nil {
|
||||||
proxySnap = s.proxy.Snapshot()
|
proxySnap = s.proxyServer().Snapshot()
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
lastError := s.lastError
|
lastError := s.lastError
|
||||||
@@ -188,11 +211,11 @@ func (s *ProxyService) GetState() ProxyState {
|
|||||||
backendRunning := false
|
backendRunning := false
|
||||||
cliHTTPSListenAddr := ""
|
cliHTTPSListenAddr := ""
|
||||||
cliHTTPSRunning := false
|
cliHTTPSRunning := false
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
backendListenAddr = s.backendHost.ListenAddr()
|
backendListenAddr = s.backend().ListenAddr()
|
||||||
backendRunning = s.backendHost.IsRunning()
|
backendRunning = s.backend().IsRunning()
|
||||||
cliHTTPSListenAddr = s.backendHost.CLIHTTPSListenAddr()
|
cliHTTPSListenAddr = s.backend().CLIHTTPSListenAddr()
|
||||||
cliHTTPSRunning = s.backendHost.CLIHTTPSRunning()
|
cliHTTPSRunning = s.backend().CLIHTTPSRunning()
|
||||||
}
|
}
|
||||||
netProxy := netproxy.CurrentStatus()
|
netProxy := netproxy.CurrentStatus()
|
||||||
return ProxyState{
|
return ProxyState{
|
||||||
@@ -263,20 +286,27 @@ func (s *ProxyService) emitState() {
|
|||||||
|
|
||||||
// ShutdownForQuit 用于处理与 ShutdownForQuit 相关的逻辑。
|
// ShutdownForQuit 用于处理与 ShutdownForQuit 相关的逻辑。
|
||||||
func (s *ProxyService) ShutdownForQuit() {
|
func (s *ProxyService) ShutdownForQuit() {
|
||||||
|
s.lifecycleMu.Lock()
|
||||||
|
defer s.lifecycleMu.Unlock()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var finalErr error
|
var finalErr error
|
||||||
|
|
||||||
if s.proxy != nil {
|
if s.proxyServer() != nil {
|
||||||
if err := s.proxy.Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
if err := s.proxyServer().Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
finalErr = err
|
finalErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
applied := s.cursorSettingsApplied
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if applied {
|
||||||
if err := s.ClearCursorSettings(); err != nil {
|
if err := s.ClearCursorSettings(); err != nil {
|
||||||
finalErr = errors.Join(finalErr, err)
|
finalErr = errors.Join(finalErr, err)
|
||||||
}
|
}
|
||||||
if s.backendHost != nil {
|
}
|
||||||
if err := s.backendHost.Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
if s.backend() != nil {
|
||||||
|
if err := s.backend().Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
finalErr = errors.Join(finalErr, err)
|
finalErr = errors.Join(finalErr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
internal/client/lifecycle_race_test.go
Normal file
33
internal/client/lifecycle_race_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
serverconfig "cursor/internal/backend/server/config"
|
||||||
|
"cursor/internal/certs"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatePollingDuringProxyInitialization(t *testing.T) {
|
||||||
|
ca, err := certs.LoadOrCreateLocalCA(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
service := &ProxyService{certManager: ca}
|
||||||
|
cfg := serverconfig.DefaultConfig()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range 300 {
|
||||||
|
_ = service.GetState()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := range 30 {
|
||||||
|
cfg.ProxyListenAddr = fmt.Sprintf("127.0.0.1:%d", 30000+i)
|
||||||
|
if err := service.ensureProxy(cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cursor/internal/logger"
|
||||||
"cursor/internal/modelchannel"
|
"cursor/internal/modelchannel"
|
||||||
|
"cursor/internal/netproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteModel 表示从模型服务 /models 端点拉取到的一个模型。
|
// RemoteModel 表示从模型服务 /models 端点拉取到的一个模型。
|
||||||
@@ -33,6 +35,7 @@ func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiK
|
|||||||
adapterType = strings.ToLower(strings.TrimSpace(adapterType))
|
adapterType = strings.ToLower(strings.TrimSpace(adapterType))
|
||||||
baseURL = strings.TrimSpace(baseURL)
|
baseURL = strings.TrimSpace(baseURL)
|
||||||
apiKey = strings.TrimSpace(apiKey)
|
apiKey = strings.TrimSpace(apiKey)
|
||||||
|
logger.RegisterSecrets(apiKey)
|
||||||
if adapterType != "openai" && adapterType != "anthropic" {
|
if adapterType != "openai" && adapterType != "anthropic" {
|
||||||
return result, fmt.Errorf("模型类型仅支持 openai 或 anthropic")
|
return result, fmt.Errorf("模型类型仅支持 openai 或 anthropic")
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,7 @@ func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiK
|
|||||||
}
|
}
|
||||||
request.Header.Set("Accept", "application/json")
|
request.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
response, err := netproxy.CredentialSafeClient(http.DefaultClient).Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("请求模型列表失败: %w", err)
|
return result, fmt.Errorf("请求模型列表失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ func (s *ProxyService) runtimeConfigSnapshot(_ context.Context) (localruntime.Ru
|
|||||||
if s == nil {
|
if s == nil {
|
||||||
return localruntime.RuntimeConfigSnapshot{}, nil
|
return localruntime.RuntimeConfigSnapshot{}, nil
|
||||||
}
|
}
|
||||||
if s.backendHost != nil && s.backendHost.ConfigManager() != nil {
|
if s.backend() != nil && s.backend().ConfigManager() != nil {
|
||||||
return s.backendHost.ConfigManager().LegacyRuntimeSnapshot(context.Background())
|
return s.backend().ConfigManager().LegacyRuntimeSnapshot(context.Background())
|
||||||
}
|
}
|
||||||
if s.store == nil {
|
if s.store == nil {
|
||||||
return localruntime.RuntimeConfigSnapshot{}, nil
|
return localruntime.RuntimeConfigSnapshot{}, nil
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
|
|
||||||
// ProxyService 定义了当前模块中的 ProxyService 类型。
|
// ProxyService 定义了当前模块中的 ProxyService 类型。
|
||||||
type ProxyService struct {
|
type ProxyService struct {
|
||||||
|
lifecycleMu sync.Mutex
|
||||||
// proxy 表示 IDE 版 MITM 代理。
|
// proxy 表示 IDE 版 MITM 代理。
|
||||||
proxy *mitm.ProxyServer
|
proxy *mitm.ProxyServer
|
||||||
// certManager 用于在代理监听地址变化时重建 MITM 服务,同时为 CLI HTTPS 监听器签发证书。
|
// certManager 用于在代理监听地址变化时重建 MITM 服务,同时为 CLI HTTPS 监听器签发证书。
|
||||||
@@ -98,34 +99,44 @@ func (s *ProxyService) ensureBackendHost() error {
|
|||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.backendHost != nil {
|
s.configMu.Lock()
|
||||||
|
defer s.configMu.Unlock()
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.backend() != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
host, err := backend.NewHost(s.store, s.certManager)
|
host, err := backend.NewHost(s.store, s.certManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.backendHost = host
|
s.setBackend(host)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProxyService) ensureProxy(cfg serverconfig.Config) error {
|
func (s *ProxyService) ensureProxy(cfg serverconfig.Config) error {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.configMu.Lock()
|
||||||
|
defer s.configMu.Unlock()
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
baseURL := ""
|
baseURL := ""
|
||||||
if s.backendHost != nil {
|
if s.backend() != nil {
|
||||||
baseURL = s.backendHost.BaseURL()
|
baseURL = s.backend().BaseURL()
|
||||||
}
|
}
|
||||||
if baseURL == "" {
|
if baseURL == "" {
|
||||||
baseURL = "http://" + cfg.BackendListenAddr
|
baseURL = "http://" + cfg.BackendListenAddr
|
||||||
}
|
}
|
||||||
listenAddr := cfg.ProxyListenAddr
|
listenAddr := cfg.ProxyListenAddr
|
||||||
|
|
||||||
if s.proxy != nil {
|
if s.proxyServer() != nil {
|
||||||
snapshot := s.proxy.Snapshot()
|
snapshot := s.proxyServer().Snapshot()
|
||||||
if snapshot.ListenAddr == listenAddr {
|
if snapshot.ListenAddr == listenAddr {
|
||||||
return s.proxy.UpdateBaseURL(baseURL)
|
return s.proxyServer().UpdateBaseURL(baseURL)
|
||||||
}
|
}
|
||||||
if snapshot.Running {
|
if snapshot.Running {
|
||||||
return fmt.Errorf("代理正在运行,不能从 %s 切换到 %s,请先停止服务", snapshot.ListenAddr, listenAddr)
|
return fmt.Errorf("代理正在运行,不能从 %s 切换到 %s,请先停止服务", snapshot.ListenAddr, listenAddr)
|
||||||
@@ -136,12 +147,12 @@ func (s *ProxyService) ensureProxy(cfg serverconfig.Config) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.proxy = proxyServer
|
s.setProxy(proxyServer)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProxyService) waitForBackend(ctx context.Context) error {
|
func (s *ProxyService) waitForBackend(ctx context.Context) error {
|
||||||
if s == nil || s.backendHost == nil {
|
if s == nil || s.backend() == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ticker := time.NewTicker(backendHealthCheckInterval)
|
ticker := time.NewTicker(backendHealthCheckInterval)
|
||||||
@@ -149,7 +160,7 @@ func (s *ProxyService) waitForBackend(ctx context.Context) error {
|
|||||||
var lastErr error
|
var lastErr error
|
||||||
for {
|
for {
|
||||||
healthCtx, healthCancel := context.WithTimeout(ctx, backendHealthCheckAttemptTimeout)
|
healthCtx, healthCancel := context.WithTimeout(ctx, backendHealthCheckAttemptTimeout)
|
||||||
err := s.backendHost.HealthCheck(healthCtx)
|
err := s.backend().HealthCheck(healthCtx)
|
||||||
healthCancel()
|
healthCancel()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -165,3 +176,24 @@ func (s *ProxyService) waitForBackend(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProxyService) backend() *backend.Host {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.backendHost
|
||||||
|
}
|
||||||
|
func (s *ProxyService) proxyServer() *mitm.ProxyServer {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.proxy
|
||||||
|
}
|
||||||
|
func (s *ProxyService) setBackend(host *backend.Host) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.backendHost = host
|
||||||
|
}
|
||||||
|
func (s *ProxyService) setProxy(proxy *mitm.ProxyServer) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.proxy = proxy
|
||||||
|
}
|
||||||
|
|||||||
37
internal/cursor/cert_migration.go
Normal file
37
internal/cursor/cert_migration.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cursor/internal/certs"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// migrateCATrust verifies both sides of the migration. Removing the legacy
|
||||||
|
// trust is necessary because its private key has already been public.
|
||||||
|
func migrateCATrust(cert []byte, trusted func([]byte) (bool, error), migrate func() error) error {
|
||||||
|
current, err := trusted(cert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
legacy, err := trusted(certs.EmbeddedCACertPEM())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current && !legacy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := migrate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current, err = trusted(cert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
legacy, err = trusted(certs.EmbeddedCACertPEM())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !current || legacy {
|
||||||
|
return fmt.Errorf("证书迁移未完成:需要信任本机 CA 并移除旧共享 CA;请重试授权")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -4,12 +4,15 @@
|
|||||||
package cursor
|
package cursor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cursor/internal/securefile"
|
||||||
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"cursor/internal/appdata"
|
"cursor/internal/appdata"
|
||||||
@@ -68,88 +71,10 @@ func DetectCLIInstallPath() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// cliPreloadFilename 是 preload 脚本的文件名。
|
const cliPreloadFilename = "tls-redirect.cjs"
|
||||||
const cliPreloadFilename = "tls-bypass.cjs"
|
|
||||||
|
|
||||||
// cliPreloadScript 是注入到 Node.js 的 preload 脚本。
|
//go:embed cli_preload.cjs
|
||||||
//
|
var cliPreloadScript string
|
||||||
// 工作原理:
|
|
||||||
// 1. patch net.connect — 将 cursor.sh 域名的 TCP 连接重定向到本地后端 HTTPS 端口
|
|
||||||
// 2. patch tls.connect — 强制 rejectUnauthorized=false,使后端签发的 CA 证书被接受
|
|
||||||
//
|
|
||||||
// CLI 的 connect-node 使用 HTTP/2 (http2.connect),goproxy 无法 MITM HTTP/2 帧。
|
|
||||||
// 因此跳过 MITM 代理,直接在 TCP 层重定向到后端的 HTTPS+HTTP/2 监听器。
|
|
||||||
// 后端 Go http.Server.ServeTLS 原生支持 HTTP/2 ALPN。
|
|
||||||
const cliPreloadScript = `// Generated by Local-Curosr: redirect cursor.sh TCP to local HTTPS backend
|
|
||||||
'use strict';
|
|
||||||
const tls = require('tls');
|
|
||||||
const net = require('net');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
const logPath = path.join(os.homedir(), '.cursor-local-assistant-v2', 'logs', 'tls-bypass.log');
|
|
||||||
function dbg(msg) {
|
|
||||||
try { fs.appendFileSync(logPath, new Date().toISOString() + ' ' + msg + '\n'); } catch(e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var REDIRECT_PORT = parseInt(process.env.CLI_HTTPS_PORT || '39092', 10);
|
|
||||||
var REDIRECT_HOST = '127.0.0.1';
|
|
||||||
var REDIRECT_DOMAINS = ['api2.cursor.sh', 'api3.cursor.sh'];
|
|
||||||
|
|
||||||
function shouldRedirect(host) {
|
|
||||||
if (!host) return false;
|
|
||||||
host = String(host).toLowerCase().trim();
|
|
||||||
for (var i = 0; i < REDIRECT_DOMAINS.length; i++) {
|
|
||||||
var d = REDIRECT_DOMAINS[i];
|
|
||||||
if (host === d || host.endsWith('.' + d)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- patch net.connect: TCP 层重定向 ---
|
|
||||||
var origNetConnect = net.connect;
|
|
||||||
function patchedNetConnect() {
|
|
||||||
if (typeof arguments[0] === 'object' && arguments[0] !== null) {
|
|
||||||
var opts = arguments[0];
|
|
||||||
if (shouldRedirect(opts.host)) {
|
|
||||||
dbg('net.connect redirect: ' + opts.host + ':' + opts.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
|
|
||||||
opts.host = REDIRECT_HOST;
|
|
||||||
opts.port = REDIRECT_PORT;
|
|
||||||
}
|
|
||||||
} else if (typeof arguments[0] === 'number' && typeof arguments[1] === 'string') {
|
|
||||||
if (shouldRedirect(arguments[1])) {
|
|
||||||
dbg('net.connect redirect (positional): ' + arguments[1] + ':' + arguments[0] + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
|
|
||||||
arguments[1] = REDIRECT_HOST;
|
|
||||||
arguments[0] = REDIRECT_PORT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return origNetConnect.apply(this, arguments);
|
|
||||||
}
|
|
||||||
net.connect = patchedNetConnect;
|
|
||||||
net.createConnection = patchedNetConnect;
|
|
||||||
|
|
||||||
// --- patch tls.connect: 强制 rejectUnauthorized=false + 重定向 ---
|
|
||||||
var origTLSConnect = tls.connect;
|
|
||||||
tls.connect = function() {
|
|
||||||
for (var i = 0; i < arguments.length && i < 3; i++) {
|
|
||||||
if (arguments[i] && typeof arguments[i] === 'object' && !Array.isArray(arguments[i])) {
|
|
||||||
var o = arguments[i];
|
|
||||||
o.rejectUnauthorized = false;
|
|
||||||
if (o.host && shouldRedirect(o.host)) {
|
|
||||||
dbg('tls.connect redirect: ' + o.host + ':' + o.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
|
|
||||||
o.host = REDIRECT_HOST;
|
|
||||||
o.port = REDIRECT_PORT;
|
|
||||||
if (!o.servername) o.servername = o.host;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return origTLSConnect.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
dbg('tls-bypass preload loaded, redirect=' + REDIRECT_HOST + ':' + REDIRECT_PORT);
|
|
||||||
`
|
|
||||||
|
|
||||||
// writeCLIPreloadScript 将 preload 脚本写入 wrapper 目录,返回文件路径。
|
// writeCLIPreloadScript 将 preload 脚本写入 wrapper 目录,返回文件路径。
|
||||||
func writeCLIPreloadScript() (string, error) {
|
func writeCLIPreloadScript() (string, error) {
|
||||||
@@ -158,7 +83,7 @@ func writeCLIPreloadScript() (string, error) {
|
|||||||
return "", fmt.Errorf("创建 preload 目录失败: %w", err)
|
return "", fmt.Errorf("创建 preload 目录失败: %w", err)
|
||||||
}
|
}
|
||||||
preloadPath := filepath.Join(wrapperDir, cliPreloadFilename)
|
preloadPath := filepath.Join(wrapperDir, cliPreloadFilename)
|
||||||
if err := os.WriteFile(preloadPath, []byte(cliPreloadScript), 0o644); err != nil {
|
if err := securefile.Write(preloadPath, []byte(cliPreloadScript)); err != nil {
|
||||||
return "", fmt.Errorf("写入 preload 脚本失败: %w", err)
|
return "", fmt.Errorf("写入 preload 脚本失败: %w", err)
|
||||||
}
|
}
|
||||||
return preloadPath, nil
|
return preloadPath, nil
|
||||||
@@ -207,15 +132,21 @@ func WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath string) error {
|
|||||||
var content string
|
var content string
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
content = buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath)
|
content = buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath)
|
||||||
|
if err := securefile.Write(filepath.Join(wrapperDir, "launch.ps1"), append([]byte{0xef, 0xbb, 0xbf}, []byte(buildWindowsLauncher(port, caCertPath, authToken, cliPath, preloadPath))...)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
content = buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath)
|
content = buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapperPath := CLIWrapperPath()
|
wrapperPath := CLIWrapperPath()
|
||||||
if err := os.WriteFile(wrapperPath, []byte(content), 0o755); err != nil {
|
if err := securefile.Write(wrapperPath, []byte(content)); err != nil {
|
||||||
return fmt.Errorf("写入 wrapper 脚本失败: %w", err)
|
return fmt.Errorf("写入 wrapper 脚本失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(wrapperPath, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Infof(
|
logger.Infof(
|
||||||
"writeCLIProxyWrapper: path=%s https_port=%s ca=%s cli=%s preload=%s",
|
"writeCLIProxyWrapper: path=%s https_port=%s ca=%s cli=%s preload=%s",
|
||||||
wrapperPath, port, caCertPath, cliPath, preloadPath,
|
wrapperPath, port, caCertPath, cliPath, preloadPath,
|
||||||
@@ -225,6 +156,11 @@ func WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath string) error {
|
|||||||
|
|
||||||
// ClearCLIProxyWrapper 移除 wrapper 脚本、preload 脚本和终端初始化脚本。
|
// ClearCLIProxyWrapper 移除 wrapper 脚本、preload 脚本和终端初始化脚本。
|
||||||
func ClearCLIProxyWrapper() error {
|
func ClearCLIProxyWrapper() error {
|
||||||
|
for _, ownedName := range []string{"launch.ps1", "tls-bypass.cjs"} {
|
||||||
|
if err := os.Remove(filepath.Join(CLIWrapperDir(), ownedName)); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
wrapperPath := CLIWrapperPath()
|
wrapperPath := CLIWrapperPath()
|
||||||
if _, err := os.Stat(wrapperPath); err != nil {
|
if _, err := os.Stat(wrapperPath); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
@@ -387,31 +323,41 @@ func ClearCLIAuthFile() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
|
func buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
|
||||||
cliDir := filepath.Dir(cliPath)
|
return "@echo off\r\nsetlocal DisableDelayedExpansion\r\n\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -ExecutionPolicy Bypass -File \"%~dp0launch.ps1\" %*\r\n"
|
||||||
psScript := filepath.Join(cliDir, "cursor-agent.ps1")
|
|
||||||
return fmt.Sprintf(`@echo off
|
|
||||||
setlocal
|
|
||||||
set "HTTP_PROXY="
|
|
||||||
set "HTTPS_PROXY="
|
|
||||||
set "CLI_HTTPS_PORT=%s"
|
|
||||||
set "NODE_EXTRA_CA_CERTS=%s"
|
|
||||||
set "NODE_TLS_REJECT_UNAUTHORIZED=0"
|
|
||||||
set "NODE_OPTIONS=--require=%s"
|
|
||||||
set "CURSOR_AUTH_TOKEN=%s"
|
|
||||||
set "CURSOR_INVOKED_AS=agent.cmd"
|
|
||||||
%%SystemRoot%%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%s" %%*
|
|
||||||
`, port, caCertPath, preloadPath, authToken, psScript)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildWindowsLauncher(port, caCertPath, authToken, cliPath, preloadPath string) string {
|
||||||
|
psScript := filepath.Join(filepath.Dir(cliPath), "cursor-agent.ps1")
|
||||||
|
return fmt.Sprintf(`$ErrorActionPreference='Stop'
|
||||||
|
$env:HTTP_PROXY=''
|
||||||
|
$env:HTTPS_PROXY=''
|
||||||
|
$env:CLI_HTTPS_PORT=%s
|
||||||
|
$env:LOCAL_CURSOR_ORIGINAL_CA_FILE=$env:NODE_EXTRA_CA_CERTS
|
||||||
|
$env:NODE_EXTRA_CA_CERTS=%s
|
||||||
|
if($env:LOCAL_CURSOR_ORIGINAL_CA_FILE -eq $env:NODE_EXTRA_CA_CERTS){$env:LOCAL_CURSOR_ORIGINAL_CA_FILE=''}
|
||||||
|
Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue
|
||||||
|
if($env:NODE_OPTIONS -match 'tls-bypass[.]cjs'){$env:NODE_OPTIONS=''}
|
||||||
|
$env:NODE_OPTIONS=($env:NODE_OPTIONS + ' ' + %s).Trim()
|
||||||
|
$env:CURSOR_AUTH_TOKEN=%s
|
||||||
|
$env:CURSOR_INVOKED_AS='agent.cmd'
|
||||||
|
& %s @args
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
`, psLiteral(port), psLiteral(caCertPath), psLiteral("--require="+strconv.Quote(preloadPath)), psLiteral(authToken), psLiteral(psScript))
|
||||||
|
}
|
||||||
|
|
||||||
|
func psLiteral(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
|
||||||
|
func shellLiteral(s string) string { return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'" }
|
||||||
|
|
||||||
func buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
|
func buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
|
||||||
return fmt.Sprintf(`#!/bin/sh
|
return fmt.Sprintf(`#!/bin/sh
|
||||||
unset HTTP_PROXY
|
unset HTTP_PROXY HTTPS_PROXY NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
unset HTTPS_PROXY
|
export CLI_HTTPS_PORT=%s
|
||||||
export CLI_HTTPS_PORT="%s"
|
export LOCAL_CURSOR_ORIGINAL_CA_FILE="${NODE_EXTRA_CA_CERTS-}"
|
||||||
export NODE_EXTRA_CA_CERTS="%s"
|
export NODE_EXTRA_CA_CERTS=%s
|
||||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
if [ "$LOCAL_CURSOR_ORIGINAL_CA_FILE" = "$NODE_EXTRA_CA_CERTS" ]; then unset LOCAL_CURSOR_ORIGINAL_CA_FILE; fi
|
||||||
export NODE_OPTIONS="--require=%s"
|
case "${NODE_OPTIONS-}" in *tls-bypass.cjs*) unset NODE_OPTIONS;; esac
|
||||||
export CURSOR_AUTH_TOKEN="%s"
|
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }"%s
|
||||||
exec "%s" "$@"
|
export CURSOR_AUTH_TOKEN=%s
|
||||||
`, port, caCertPath, preloadPath, authToken, cliPath)
|
exec %s "$@"
|
||||||
|
`, shellLiteral(port), shellLiteral(caCertPath), shellLiteral("--require="+strconv.Quote(preloadPath)), shellLiteral(authToken), shellLiteral(cliPath))
|
||||||
}
|
}
|
||||||
|
|||||||
43
internal/cursor/cli_preload.cjs
Normal file
43
internal/cursor/cli_preload.cjs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Generated by Local-Cursor. Redirect only Cursor API hosts; keep TLS verification.
|
||||||
|
'use strict';
|
||||||
|
const tls = require('node:tls');
|
||||||
|
const net = require('node:net');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const {X509Certificate} = require('node:crypto');
|
||||||
|
const port = Number(process.env.CLI_HTTPS_PORT || 39092);
|
||||||
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid CLI HTTPS port');
|
||||||
|
const localCA = process.env.NODE_EXTRA_CA_CERTS ? fs.readFileSync(process.env.NODE_EXTRA_CA_CERTS) : undefined;
|
||||||
|
const originalCAPath = process.env.LOCAL_CURSOR_ORIGINAL_CA_FILE;
|
||||||
|
const extraCA = originalCAPath ? (fs.readFileSync(originalCAPath, 'utf8').match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) || []).filter(cert => new X509Certificate(cert).fingerprint256.replaceAll(':','').toLowerCase() !== '836e6bb84f6c3e63316dbb4ec257223af09f7490e7aae09030b8515ed61ee9ff') : [];
|
||||||
|
const redirect = host => /^(?:[a-z0-9-]+\.)*api[23]\.cursor\.sh$/i.test(String(host || ''));
|
||||||
|
|
||||||
|
function normalized(args) {
|
||||||
|
if (args[0] && typeof args[0] === 'object') return { options: { ...args[0] }, callback: args.find(a => typeof a === 'function') };
|
||||||
|
const extra = args.find(a => a && typeof a === 'object') || {};
|
||||||
|
return { options: { ...extra, port: args[0], ...(typeof args[1] === 'string' ? {host: args[1]} : {}) }, callback: args.find(a => typeof a === 'function') };
|
||||||
|
}
|
||||||
|
const originalNetConnect = net.connect;
|
||||||
|
function connect(...args) {
|
||||||
|
const {options, callback} = normalized(args);
|
||||||
|
if (!redirect(options.host)) return originalNetConnect.apply(this, args);
|
||||||
|
options.host = '127.0.0.1'; options.port = port;
|
||||||
|
return originalNetConnect.call(this, options, ...(callback ? [callback] : []));
|
||||||
|
}
|
||||||
|
net.connect = net.createConnection = connect;
|
||||||
|
const originalTLSConnect = tls.connect;
|
||||||
|
tls.connect = function (...args) {
|
||||||
|
const {options, callback} = normalized(args);
|
||||||
|
if (!redirect(options.host)) {
|
||||||
|
if (!extraCA.length || options.ca !== undefined || options.secureContext) return originalTLSConnect.apply(this, args);
|
||||||
|
const defaults = typeof tls.getCACertificates === 'function' ? tls.getCACertificates('default') : tls.rootCertificates;
|
||||||
|
options.ca = [...defaults, ...extraCA];
|
||||||
|
return originalTLSConnect.call(this, options, ...(callback ? [callback] : []));
|
||||||
|
}
|
||||||
|
const originalHost = options.host;
|
||||||
|
options.host = '127.0.0.1'; options.port = port;
|
||||||
|
options.servername = options.servername || originalHost;
|
||||||
|
options.rejectUnauthorized = true;
|
||||||
|
if (!options.ca && extraCA.length) options.ca = [...(typeof tls.getCACertificates === "function" ? tls.getCACertificates("default") : tls.rootCertificates), ...extraCA];
|
||||||
|
if (options.ca && localCA) options.ca = [...(Array.isArray(options.ca) ? options.ca : [options.ca]), localCA];
|
||||||
|
return originalTLSConnect.call(this, options, ...(callback ? [callback] : []));
|
||||||
|
};
|
||||||
89
internal/cursor/cli_security_test.go
Normal file
89
internal/cursor/cli_security_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cursor/internal/certs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLIHTTP2TrustSNIAndUnrelatedTLSRejection(t *testing.T) {
|
||||||
|
if _, err := exec.LookPath("node"); err != nil {
|
||||||
|
t.Fatal("Node.js is required for CLI TLS integration tests")
|
||||||
|
}
|
||||||
|
dir := filepath.Join(t.TempDir(), "中文 space ' test")
|
||||||
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
manager, err := certs.LoadOrCreateLocalCA(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.ProtoMajor != 2 {
|
||||||
|
t.Error("not HTTP/2")
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte("secure-http2"))
|
||||||
|
}))
|
||||||
|
server.EnableHTTP2 = true
|
||||||
|
server.TLS = &tls.Config{GetCertificate: func(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return manager.CertificateForServerName(h.ServerName)
|
||||||
|
}}
|
||||||
|
server.StartTLS()
|
||||||
|
defer server.Close()
|
||||||
|
u, _ := url.Parse(server.URL)
|
||||||
|
preload := filepath.Join(dir, cliPreloadFilename)
|
||||||
|
if err = os.WriteFile(preload, []byte(cliPreloadScript), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
good := `const h=require('node:http2');const s=h.connect('https://api2.cursor.sh');s.on('error',e=>{console.error(e.code);process.exitCode=1});const r=s.request({':path':'/'});let b='';r.on('data',d=>b+=d);r.on('end',()=>{if(b!=='secure-http2')process.exitCode=2;s.close()});r.end();`
|
||||||
|
bad := fmt.Sprintf(`const t=require('node:tls');const s=t.connect({host:'127.0.0.1',port:%s},()=>{console.error('accepted invalid host');s.destroy();process.exitCode=2});s.on('error',()=>process.exit(0));`, u.Port())
|
||||||
|
for _, script := range []string{good, bad} {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
cmd := exec.CommandContext(ctx, "node", "-e", script)
|
||||||
|
cmd.Env = append(os.Environ(), "CLI_HTTPS_PORT="+u.Port(), "NODE_EXTRA_CA_CERTS="+filepath.Join(dir, "ca.crt"), "NODE_OPTIONS=--require="+strconv.Quote(preload), "NODE_TLS_REJECT_UNAUTHORIZED=")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CLI integration: %v %s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrong, err := certs.LoadOrCreateLocalCA(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wrongPath := filepath.Join(dir, "wrong.crt")
|
||||||
|
if err = os.WriteFile(wrongPath, wrong.CACertPEM(), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "node", "-e", `const h=require('node:http2');const s=h.connect('https://api2.cursor.sh');s.on('connect',()=>{s.close();process.exitCode=2});s.on('error',()=>process.exit(0));`)
|
||||||
|
cmd.Env = append(os.Environ(), "CLI_HTTPS_PORT="+u.Port(), "NODE_EXTRA_CA_CERTS="+wrongPath, "NODE_OPTIONS=--require="+strconv.Quote(preload), "NODE_TLS_REJECT_UNAUTHORIZED=")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("wrong-CA rejection: %v %s", err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapperQuotesPathsAndDoesNotDisableTLS(t *testing.T) {
|
||||||
|
path := `C:\用户 space\O'Brien\tls-redirect.cjs`
|
||||||
|
ps := buildWindowsLauncher("39092", `C:\用户 space\ca.crt`, "test-token", `C:\用户 space\agent.cmd`, path)
|
||||||
|
if strings.Contains(ps, "NODE_TLS_REJECT_UNAUTHORIZED=0") || !strings.Contains(ps, "O''Brien") || !strings.Contains(ps, "--require=") {
|
||||||
|
t.Fatal("unsafe PowerShell launcher")
|
||||||
|
}
|
||||||
|
sh := buildUnixWrapper("39092", "/tmp/user ' space/ca.crt", "test-token", "/tmp/bin ' /agent", "/tmp/user ' space/preload.cjs")
|
||||||
|
if strings.Contains(sh, "REJECT_UNAUTHORIZED=0") || !strings.Contains(sh, `'"'"'`) {
|
||||||
|
t.Fatal("unsafe shell wrapper")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package cursor
|
|||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"cursor/internal/certs"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -84,15 +85,23 @@ func installCACertToDarwinKeychain(certPEM []byte, certPath string) error {
|
|||||||
|
|
||||||
// EnsureCACertInstalled 确保 CA 证书已安装到 macOS 登录钥匙串。
|
// EnsureCACertInstalled 确保 CA 证书已安装到 macOS 登录钥匙串。
|
||||||
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
||||||
installed, err := isCACertInstalled(certPEM)
|
return migrateCATrust(certPEM, isCACertInstalled, func() error {
|
||||||
|
if err := installCACertToDarwinKeychain(certPEM, certPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
legacy, err := getCertSHA1Fingerprint(certs.EmbeddedCACertPEM())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("检查 macOS 证书安装状态失败: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
exists, err := isCACertInstalled(certs.EmbeddedCACertPEM())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
if out, err := exec.Command(darwinSecurityExe, "delete-certificate", "-t", "-Z", legacy, darwinLoginKeychainName).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("移除旧共享证书失败: %w: %s", err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if installed {
|
|
||||||
logger.Infof("ensureCACertInstalled: cert already installed in macOS login keychain, skipping")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
})
|
||||||
|
|
||||||
logger.Infof("ensureCACertInstalled: cert not installed in macOS login keychain, installing...")
|
|
||||||
return installCACertToDarwinKeychain(certPEM, certPath)
|
|
||||||
}
|
}
|
||||||
|
|||||||
77
internal/cursor/launcher_windows_test.go
Normal file
77
internal/cursor/launcher_windows_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func utf8BOM(text string) []byte { return append([]byte{0xef, 0xbb, 0xbf}, []byte(text)...) }
|
||||||
|
|
||||||
|
func TestPowerShellLauncherPreservesUnicodePathsAndArguments(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "中文 space O'Brien")
|
||||||
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result := filepath.Join(dir, "result.json")
|
||||||
|
fake := `$payload=@{args=@($args);ca=$env:NODE_EXTRA_CA_CERTS;options=$env:NODE_OPTIONS;tls=$env:NODE_TLS_REJECT_UNAUTHORIZED};[IO.File]::WriteAllText(` + psLiteral(result) + `,($payload|ConvertTo-Json -Compress),[Text.Encoding]::UTF8);exit 0`
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "cursor-agent.ps1"), utf8BOM(fake), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
preload := filepath.Join(dir, "tls-redirect.cjs")
|
||||||
|
launcher := filepath.Join(dir, "launch.ps1")
|
||||||
|
if err := os.WriteFile(launcher, utf8BOM(buildWindowsLauncher("39092", filepath.Join(dir, "ca.crt"), "fixture-token", filepath.Join(dir, "agent.cmd"), preload)), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", launcher, "--message", "one two", "a&b", "O'Brien")
|
||||||
|
cmd.Env = append(os.Environ(), "NODE_TLS_REJECT_UNAUTHORIZED=0", "NODE_OPTIONS=--require=old-tls-bypass.cjs")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("launcher: %v %s", err, out)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data = []byte(strings.TrimPrefix(string(data), "\ufeff"))
|
||||||
|
var got struct {
|
||||||
|
Args []string `json:"args"`
|
||||||
|
CA string `json:"ca"`
|
||||||
|
Options string `json:"options"`
|
||||||
|
TLS string `json:"tls"`
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(data, &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []string{"--message", "one two", "a&b", "O'Brien"}
|
||||||
|
if len(got.Args) != len(want) {
|
||||||
|
t.Fatalf("arguments: %#v", got.Args)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got.Args[i] != want[i] {
|
||||||
|
t.Fatalf("argument %d: %q", i, got.Args[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got.CA != filepath.Join(dir, "ca.crt") || got.TLS != "" || strings.Contains(got.Options, "tls-bypass") {
|
||||||
|
t.Fatal("unsafe launcher environment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrationPowerShellSyntaxWithoutExecutingTrustChanges(t *testing.T) {
|
||||||
|
body := windowsCAMigrationBody(`C:\用户 space O'Brien\ca.crt`, strings.Repeat("A", 40), strings.Repeat("B", 40))
|
||||||
|
path := filepath.Join(t.TempDir(), "migration.ps1")
|
||||||
|
if err := os.WriteFile(path, utf8BOM(body), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parse := `$tokens=$null;$errors=$null;$null=[Management.Automation.Language.Parser]::ParseFile(` + psLiteral(path) + `,[ref]$tokens,[ref]$errors);if($errors.Count){$errors|ForEach-Object{$_.Message};exit 1}`
|
||||||
|
if out, err := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-Command", parse).CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("invalid migration syntax: %v %s", err, out)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "CurrentUser") {
|
||||||
|
t.Fatal("elevated script must not target the alternate administrator's user store")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,8 @@ func mergeNodeSearchPath() (string, bool) {
|
|||||||
func detectCursorInstallPathFromRegistry() string {
|
func detectCursorInstallPathFromRegistry() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scanDriveRootsForCursor is only used as a Windows fallback.
|
||||||
|
func scanDriveRootsForCursor() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,8 +179,16 @@ func DeleteProjectRecord(projectPath string) error {
|
|||||||
return fmt.Errorf("未找到 Cursor 用户数据目录: %s", userDir)
|
return fmt.Errorf("未找到 Cursor 用户数据目录: %s", userDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
storageRemoved, storageErr := removeWorkspaceStorageEntries(userDir, projectPath)
|
return deleteProjectRecords(userDir, projectPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteProjectRecords(userDir, projectPath string) error {
|
||||||
|
// Never delete chat data before confirming that the recent-list database can be updated.
|
||||||
entryRemoved, entryErr := removeRecentlyOpenedEntry(userDir, projectPath)
|
entryRemoved, entryErr := removeRecentlyOpenedEntry(userDir, projectPath)
|
||||||
|
if entryErr != nil {
|
||||||
|
return entryErr
|
||||||
|
}
|
||||||
|
storageRemoved, storageErr := removeWorkspaceStorageEntries(userDir, projectPath)
|
||||||
if storageErr != nil || entryErr != nil {
|
if storageErr != nil || entryErr != nil {
|
||||||
return fmt.Errorf("删除不完整(workspaceStorage: %d 处, %v;最近打开列表: %d 处, %v)",
|
return fmt.Errorf("删除不完整(workspaceStorage: %d 处, %v;最近打开列表: %d 处, %v)",
|
||||||
storageRemoved, storageErr, entryRemoved, entryErr)
|
storageRemoved, storageErr, entryRemoved, entryErr)
|
||||||
@@ -255,32 +263,40 @@ func removeRecentlyOpenedEntry(userDir string, projectPath string) (int, error)
|
|||||||
}
|
}
|
||||||
return 0, fmt.Errorf("读取最近打开列表失败: %w", err)
|
return 0, fmt.Errorf("读取最近打开列表失败: %w", err)
|
||||||
}
|
}
|
||||||
var payload struct {
|
var payload map[string]json.RawMessage
|
||||||
Entries []struct {
|
|
||||||
FolderURI string `json:"folderUri"`
|
|
||||||
} `json:"entries"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
return 0, fmt.Errorf("解析最近打开列表失败: %w", err)
|
return 0, fmt.Errorf("解析最近打开列表失败: %w", err)
|
||||||
}
|
}
|
||||||
kept := make([]struct {
|
var entries []json.RawMessage
|
||||||
FolderURI string `json:"folderUri"`
|
if err := json.Unmarshal(payload["entries"], &entries); err != nil {
|
||||||
}, 0, len(payload.Entries))
|
return 0, err
|
||||||
|
}
|
||||||
|
kept := make([]json.RawMessage, 0, len(entries))
|
||||||
removed := 0
|
removed := 0
|
||||||
for _, item := range payload.Entries {
|
for _, entry := range entries {
|
||||||
|
var item struct {
|
||||||
|
FolderURI string `json:"folderUri"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(entry, &item); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
path := projectPathFromURI(item.FolderURI)
|
path := projectPathFromURI(item.FolderURI)
|
||||||
if path != "" && samePath(filepath.Clean(path), projectPath) {
|
if path != "" && samePath(filepath.Clean(path), projectPath) {
|
||||||
removed++
|
removed++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kept = append(kept, item)
|
kept = append(kept, entry)
|
||||||
}
|
}
|
||||||
if removed == 0 {
|
if removed == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
updated, err := json.Marshal(map[string]any{"entries": kept})
|
payload["entries"], err = json.Marshal(kept)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("序列化最近打开列表失败: %w", err)
|
return 0, err
|
||||||
|
}
|
||||||
|
updated, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
if _, err := db.Exec(
|
if _, err := db.Exec(
|
||||||
"UPDATE ItemTable SET value = ? WHERE key = 'history.recentlyOpenedPathsList'",
|
"UPDATE ItemTable SET value = ? WHERE key = 'history.recentlyOpenedPathsList'",
|
||||||
|
|||||||
158
internal/cursor/proxy_settings.go
Normal file
158
internal/cursor/proxy_settings.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"cursor/internal/appdata"
|
||||||
|
"cursor/internal/securefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
var proxySettingsMu sync.Mutex
|
||||||
|
|
||||||
|
type proxySettingsBackup struct {
|
||||||
|
Original map[string]json.RawMessage `json:"original"`
|
||||||
|
Applied map[string]json.RawMessage `json:"applied"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsBackupPath() string {
|
||||||
|
return filepath.Join(appdata.DataRootPath(), "proxy-settings-backup.json")
|
||||||
|
}
|
||||||
|
func rawEqual(a, b json.RawMessage) bool {
|
||||||
|
var x, y bytes.Buffer
|
||||||
|
if json.Compact(&x, a) != nil || json.Compact(&y, b) != nil {
|
||||||
|
return len(a) == 0 && len(b) == 0
|
||||||
|
}
|
||||||
|
return bytes.Equal(x.Bytes(), y.Bytes())
|
||||||
|
}
|
||||||
|
func loadSettings(path string) (map[string]json.RawMessage, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return map[string]json.RawMessage{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = normalizeJSONC(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(bytes.TrimSpace(data)) == 0 {
|
||||||
|
return map[string]json.RawMessage{}, nil
|
||||||
|
}
|
||||||
|
var settings map[string]json.RawMessage
|
||||||
|
if err = json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if settings == nil {
|
||||||
|
return nil, fmt.Errorf("Cursor 配置必须是 JSON 对象")
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
func readSettingsBackup() (proxySettingsBackup, error) {
|
||||||
|
backup := proxySettingsBackup{Original: map[string]json.RawMessage{}, Applied: map[string]json.RawMessage{}}
|
||||||
|
data, err := os.ReadFile(settingsBackupPath())
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return backup, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return backup, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &backup)
|
||||||
|
if err == nil && (backup.Original == nil || backup.Applied == nil) {
|
||||||
|
err = fmt.Errorf("代理配置备份损坏")
|
||||||
|
}
|
||||||
|
return backup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteUserProxySettings(proxyURL string) error {
|
||||||
|
if proxyURL == "" {
|
||||||
|
return fmt.Errorf("代理地址为空")
|
||||||
|
}
|
||||||
|
proxySettingsMu.Lock()
|
||||||
|
defer proxySettingsMu.Unlock()
|
||||||
|
path, err := resolveCursorSettingsPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settings, err := loadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析 Cursor 配置失败,原文件已保留: %w", err)
|
||||||
|
}
|
||||||
|
backup, err := readSettingsBackup()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := map[string]any{"http.proxy": proxyURL, "http.proxyKerberosServicePrincipal": proxyURL, "http.proxySupport": "on", "cursor.general.disableHttp2": true, "http.experimental.systemCertificatesV2": true}
|
||||||
|
for key, value := range values {
|
||||||
|
if previous, exists := settings[key]; exists {
|
||||||
|
if !rawEqual(previous, backup.Applied[key]) {
|
||||||
|
backup.Original[key] = append(json.RawMessage(nil), previous...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete(backup.Original, key)
|
||||||
|
}
|
||||||
|
encoded, _ := json.Marshal(value)
|
||||||
|
backup.Applied[key] = encoded
|
||||||
|
settings[key] = encoded
|
||||||
|
}
|
||||||
|
backupData, err := json.Marshal(backup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = securefile.Write(settingsBackupPath(), backupData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Preserve the product's intentional permanent auto-update setting.
|
||||||
|
settings["update.mode"] = json.RawMessage(`"none"`)
|
||||||
|
data, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return securefile.Write(path, append(data, '\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearUserProxySettings() error {
|
||||||
|
proxySettingsMu.Lock()
|
||||||
|
defer proxySettingsMu.Unlock()
|
||||||
|
path, err := resolveCursorSettingsPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
backup, err := readSettingsBackup()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(backup.Applied) == 0 {
|
||||||
|
return nil
|
||||||
|
} // No ownership proof: preserve user values.
|
||||||
|
settings, err := loadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析 Cursor 配置失败,原文件已保留: %w", err)
|
||||||
|
}
|
||||||
|
for key, applied := range backup.Applied {
|
||||||
|
if rawEqual(settings[key], applied) {
|
||||||
|
if previous, exists := backup.Original[key]; exists {
|
||||||
|
settings[key] = previous
|
||||||
|
} else {
|
||||||
|
delete(settings, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = securefile.Write(path, append(data, '\n')); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.Remove(settingsBackupPath()); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
167
internal/cursor/security_regression_test.go
Normal file
167
internal/cursor/security_regression_test.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package cursor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cursor/internal/certs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isolatedHome(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
for _, key := range []string{"HOME", "USERPROFILE", "APPDATA", "XDG_CONFIG_HOME"} {
|
||||||
|
t.Setenv(key, dir)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxySettingsRestoreAndPreserveConcurrentUserChange(t *testing.T) {
|
||||||
|
isolatedHome(t)
|
||||||
|
path, err := resolveCursorSettingsPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
original := []byte(`{"http.proxy":"http://original:8080","editor.fontSize":15,"custom":{"enabled":true}}`)
|
||||||
|
if err = os.WriteFile(path, original, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
if err = WriteUserProxySettings("http://127.0.0.1:28080"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current, err := loadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
current["http.proxySupport"] = json.RawMessage(`"override"`)
|
||||||
|
data, _ := json.Marshal(current)
|
||||||
|
if err = os.WriteFile(path, data, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = ClearUserProxySettings(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := loadSettings(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !rawEqual(got["http.proxy"], json.RawMessage(`"http://original:8080"`)) {
|
||||||
|
t.Fatal("original proxy not restored")
|
||||||
|
}
|
||||||
|
if !rawEqual(got["http.proxySupport"], json.RawMessage(`"override"`)) {
|
||||||
|
t.Fatal("user edit overwritten")
|
||||||
|
}
|
||||||
|
if !rawEqual(got["custom"], json.RawMessage(`{"enabled":true}`)) {
|
||||||
|
t.Fatal("unrelated settings lost")
|
||||||
|
}
|
||||||
|
if _, ok := got["cursor.general.disableHttp2"]; ok {
|
||||||
|
t.Fatal("injected key not removed")
|
||||||
|
}
|
||||||
|
if err = ClearUserProxySettings(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidSettingsNeverDeleted(t *testing.T) {
|
||||||
|
isolatedHome(t)
|
||||||
|
path, _ := resolveCursorSettingsPath()
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0700)
|
||||||
|
bad := []byte("{broken user settings // keep me")
|
||||||
|
if err := os.WriteFile(path, bad, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := WriteUserProxySettings("http://127.0.0.1:28080"); err == nil {
|
||||||
|
t.Fatal("invalid settings accepted")
|
||||||
|
}
|
||||||
|
after, _ := os.ReadFile(path)
|
||||||
|
if !bytes.Equal(bad, after) {
|
||||||
|
t.Fatal("user settings destroyed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateMigrationCancellationAndRetry(t *testing.T) {
|
||||||
|
newCert := []byte("local CA")
|
||||||
|
current, legacy := false, true
|
||||||
|
trusted := func(cert []byte) (bool, error) {
|
||||||
|
if bytes.Equal(cert, certs.EmbeddedCACertPEM()) {
|
||||||
|
return legacy, nil
|
||||||
|
}
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
denied := errors.New("authorization canceled")
|
||||||
|
if err := migrateCATrust(newCert, trusted, func() error { return denied }); !errors.Is(err, denied) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if current || !legacy {
|
||||||
|
t.Fatal("cancellation changed trust")
|
||||||
|
}
|
||||||
|
if err := migrateCATrust(newCert, trusted, func() error { current = true; legacy = false; return nil }); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := migrateCATrust(newCert, trusted, func() error { t.Fatal("migration not idempotent"); return nil }); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
legacy = true
|
||||||
|
if err := migrateCATrust(newCert, trusted, func() error { return nil }); err == nil {
|
||||||
|
t.Fatal("legacy trust silently retained")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecentlyOpenedPreservesUnknownFields(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
createStateDB(t, dir, []string{"file:///d:/Project/A", "file:///d:/Project/B"})
|
||||||
|
db, err := sql.Open("sqlite", filepath.Join(dir, "globalStorage", "state.vscdb"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
raw := `{"version":3,"metadata":{"keep":true},"entries":[{"folderUri":"file:///d:/Project/A","label":"remove"},{"folderUri":"file:///d:/Project/B","label":"keep","remoteAuthority":"ssh-remote+host"},{"fileUri":"file:///d:/readme.md","label":"file"}]}`
|
||||||
|
if _, err = db.Exec("UPDATE ItemTable SET value = ? WHERE key = 'history.recentlyOpenedPathsList'", raw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n, err := removeRecentlyOpenedEntry(dir, "d:/Project/A"); err != nil || n != 1 {
|
||||||
|
t.Fatalf("removed=%d err=%v", n, err)
|
||||||
|
}
|
||||||
|
var updated string
|
||||||
|
if err = db.QueryRow("SELECT value FROM ItemTable WHERE key = 'history.recentlyOpenedPathsList'").Scan(&updated); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got map[string]json.RawMessage
|
||||||
|
if err = json.Unmarshal([]byte(updated), &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !rawEqual(got["metadata"], json.RawMessage(`{"keep":true}`)) || !rawEqual(got["version"], json.RawMessage(`3`)) {
|
||||||
|
t.Fatal("top-level metadata lost")
|
||||||
|
}
|
||||||
|
var entries []map[string]any
|
||||||
|
_ = json.Unmarshal(got["entries"], &entries)
|
||||||
|
if len(entries) != 2 || entries[0]["remoteAuthority"] != "ssh-remote+host" || entries[1]["fileUri"] != "file:///d:/readme.md" {
|
||||||
|
t.Fatalf("unmatched entries changed: %s", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDatabaseFailurePreservesWorkspaceChatData(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
workspace := filepath.Join(dir, "workspaceStorage", "hash")
|
||||||
|
_ = os.MkdirAll(workspace, 0700)
|
||||||
|
_ = os.WriteFile(filepath.Join(workspace, "workspace.json"), []byte(`{"folder":"file:///d:/Project/A"}`), 0600)
|
||||||
|
_ = os.WriteFile(filepath.Join(workspace, "chat.db"), []byte("important user chat"), 0600)
|
||||||
|
_ = os.MkdirAll(filepath.Join(dir, "globalStorage"), 0700)
|
||||||
|
_ = os.WriteFile(filepath.Join(dir, "globalStorage", "state.vscdb"), []byte("corrupt database"), 0600)
|
||||||
|
if err := deleteProjectRecords(dir, "d:/Project/A"); err == nil {
|
||||||
|
t.Fatal("corrupt database accepted")
|
||||||
|
}
|
||||||
|
if data, err := os.ReadFile(filepath.Join(workspace, "chat.db")); err != nil || string(data) != "important user chat" {
|
||||||
|
t.Fatal("chat data removed before database update")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,12 +30,6 @@ var injectedCursorSettingsKeys = []string{
|
|||||||
// EnsureCACertFile 用于处理与 EnsureCACertFile 相关的逻辑。
|
// EnsureCACertFile 用于处理与 EnsureCACertFile 相关的逻辑。
|
||||||
func EnsureCACertFile(certPEM []byte, currentPath string) (string, error) {
|
func EnsureCACertFile(certPEM []byte, currentPath string) (string, error) {
|
||||||
certPath := appdata.CACertFilePath()
|
certPath := appdata.CACertFilePath()
|
||||||
if samePath(strings.TrimSpace(currentPath), certPath) {
|
|
||||||
if _, err := os.Stat(certPath); err == nil {
|
|
||||||
logger.Infof("ensureCACertFile: reusing path=%s", certPath)
|
|
||||||
return certPath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
|
||||||
return "", fmt.Errorf("创建证书配置目录失败: %w", err)
|
return "", fmt.Errorf("创建证书配置目录失败: %w", err)
|
||||||
@@ -65,6 +59,9 @@ func samePath(left string, right string) bool {
|
|||||||
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
|
||||||
|
}
|
||||||
return filepath.Clean(left) == filepath.Clean(right)
|
return filepath.Clean(left) == filepath.Clean(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,125 +114,6 @@ func ClearSystemNodeExtraCACerts() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteUserProxySettings 用于处理与 WriteUserProxySettings 相关的逻辑。
|
|
||||||
func WriteUserProxySettings(proxyURL string) error {
|
|
||||||
proxyURL = strings.TrimSpace(proxyURL)
|
|
||||||
if proxyURL == "" {
|
|
||||||
return errors.New("代理地址为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsPath, err := resolveCursorSettingsPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("创建 Cursor 配置目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := make(map[string]any)
|
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return fmt.Errorf("读取 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
} else if len(bytes.TrimSpace(data)) > 0 {
|
|
||||||
parsed, err := decodeCursorSettingsJSONC(data)
|
|
||||||
if err != nil {
|
|
||||||
if removeErr := os.Remove(settingsPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
|
||||||
return fmt.Errorf("解析 Cursor 配置失败,且删除损坏配置失败: %w", removeErr)
|
|
||||||
}
|
|
||||||
logger.Infof("writeCursorUserProxySettings: removed invalid settings path=%s err=%v", settingsPath, err)
|
|
||||||
data = nil
|
|
||||||
} else {
|
|
||||||
settings = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
settings["http.proxy"] = proxyURL
|
|
||||||
settings["http.proxyKerberosServicePrincipal"] = proxyURL
|
|
||||||
settings["http.proxySupport"] = "on"
|
|
||||||
settings["cursor.general.disableHttp2"] = true
|
|
||||||
settings["http.experimental.systemCertificatesV2"] = true
|
|
||||||
// update.mode 写入后永久保留,不随 ClearUserProxySettings 清除,避免恢复自动更新。
|
|
||||||
settings["update.mode"] = "none"
|
|
||||||
|
|
||||||
encoded, err := json.MarshalIndent(settings, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("序列化 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
encoded = append(encoded, '\n')
|
|
||||||
|
|
||||||
if len(bytes.TrimSpace(data)) > 0 && bytes.Equal(data, encoded) {
|
|
||||||
logger.Infof("writeCursorUserProxySettings: unchanged path=%s proxy=%s", settingsPath, proxyURL)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tempPath := settingsPath + ".tmp"
|
|
||||||
if err := os.WriteFile(tempPath, encoded, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("写入 Cursor 配置临时文件失败: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(tempPath, settingsPath); err != nil {
|
|
||||||
return fmt.Errorf("保存 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("writeCursorUserProxySettings: path=%s proxy=%s", settingsPath, proxyURL)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearUserProxySettings 用于处理与 ClearUserProxySettings 相关的逻辑。
|
|
||||||
func ClearUserProxySettings() error {
|
|
||||||
settingsPath, err := resolveCursorSettingsPath()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("读取 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
if len(bytes.TrimSpace(data)) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := make(map[string]any)
|
|
||||||
parsed, err := decodeCursorSettingsJSONC(data)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
settings = parsed
|
|
||||||
|
|
||||||
changed := false
|
|
||||||
for _, key := range injectedCursorSettingsKeys {
|
|
||||||
if _, exists := settings[key]; exists {
|
|
||||||
delete(settings, key)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !changed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded, err := json.MarshalIndent(settings, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("序列化 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
encoded = append(encoded, '\n')
|
|
||||||
|
|
||||||
tempPath := settingsPath + ".tmp"
|
|
||||||
if err := os.WriteFile(tempPath, encoded, 0o644); err != nil {
|
|
||||||
return fmt.Errorf("写入 Cursor 配置临时文件失败: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(tempPath, settingsPath); err != nil {
|
|
||||||
return fmt.Errorf("保存 Cursor 配置失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("clearCursorUserProxySettings: path=%s", settingsPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveCursorSettingsPath 用于处理与 resolveCursorSettingsPath 相关的逻辑。
|
// resolveCursorSettingsPath 用于处理与 resolveCursorSettingsPath 相关的逻辑。
|
||||||
func resolveCursorSettingsPath() (string, error) {
|
func resolveCursorSettingsPath() (string, error) {
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ package cursor
|
|||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"cursor/internal/certs"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
"cursor/internal/logger"
|
"cursor/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -147,16 +151,62 @@ func installCACertToWindowsStore(certPEM []byte, certPath string) error {
|
|||||||
|
|
||||||
// EnsureCACertInstalled 确保证书已安装到 Windows 系统信任存储。
|
// EnsureCACertInstalled 确保证书已安装到 Windows 系统信任存储。
|
||||||
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
||||||
installed, err := isCACertInstalled(certPEM)
|
return migrateCATrust(certPEM, isCAInstalledAnyStore, func() error {
|
||||||
|
thumb, err := getCertThumbprint(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("检查系统证书安装状态失败: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
legacy, err := getCertThumbprint(certs.EmbeddedCACertPEM())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := windowsCAMigrationBody(certPath, thumb, legacy)
|
||||||
|
units := utf16.Encode([]rune(body))
|
||||||
|
bytes := make([]byte, len(units)*2)
|
||||||
|
for i, u := range units {
|
||||||
|
binary.LittleEndian.PutUint16(bytes[i*2:], u)
|
||||||
|
}
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||||
|
outer := "$p=Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','" + encoded + "') -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode"
|
||||||
|
cmd := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", outer)
|
||||||
|
cmd.SysProcAttr = hideWindow()
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("安装证书授权失败或已取消,请重试: %w", err)
|
||||||
|
}
|
||||||
|
cleanup := fmt.Sprintf("$ErrorActionPreference='Stop'; $s=New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser'); try { $s.Open('ReadWrite'); foreach($c in @($s.Certificates.Find('FindByThumbprint',%s,$false))) { $s.Remove($c) } } finally { $s.Close() }", quotePowerShellLiteral(legacy))
|
||||||
|
originalUser := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", cleanup)
|
||||||
|
originalUser.SysProcAttr = hideWindow()
|
||||||
|
if err := originalUser.Run(); err != nil {
|
||||||
|
return fmt.Errorf("移除当前用户的旧共享证书失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if installed {
|
|
||||||
logger.Infof("ensureCACertInstalled: cert already installed in system store, skipping")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
})
|
||||||
|
}
|
||||||
logger.Infof("ensureCACertInstalled: cert not installed in system store, installing...")
|
|
||||||
return installCACertToWindowsStore(certPEM, certPath)
|
func isCAInstalledAnyStore(certPEM []byte) (bool, error) {
|
||||||
|
thumb, err := getCertThumbprint(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
script := "$ErrorActionPreference='Stop'; (Test-Path " + quotePowerShellLiteral(`Cert:\LocalMachine\Root\`+thumb) + ") -or (Test-Path " + quotePowerShellLiteral(`Cert:\CurrentUser\Root\`+thumb) + ")"
|
||||||
|
cmd := exec.Command(windowsPowerShellExe, "-NoProfile", "-NonInteractive", "-Command", script)
|
||||||
|
cmd.SysProcAttr = hideWindow()
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strings.EqualFold(strings.TrimSpace(string(out)), "True"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowsCAMigrationBody(certPath, thumb, legacy string) string {
|
||||||
|
return fmt.Sprintf(`$ErrorActionPreference='Stop'
|
||||||
|
$c = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(%s)
|
||||||
|
if ($c.Thumbprint -ne %s) { throw 'CA fingerprint mismatch' }
|
||||||
|
$s = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine')
|
||||||
|
try {
|
||||||
|
$s.Open('ReadWrite'); $s.Add($c)
|
||||||
|
if ($s.Certificates.Find('FindByThumbprint',%s,$false).Count -eq 0) { throw 'CA installation failed' }
|
||||||
|
foreach ($old in @($s.Certificates.Find('FindByThumbprint',%s,$false))) { $s.Remove($old) }
|
||||||
|
} finally { $s.Close() }
|
||||||
|
`, quotePowerShellLiteral(certPath), quotePowerShellLiteral(thumb), quotePowerShellLiteral(thumb), quotePowerShellLiteral(legacy))
|
||||||
}
|
}
|
||||||
|
|||||||
46
internal/localaccess/origin.go
Normal file
46
internal/localaccess/origin.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Package localaccess prevents unrelated browser pages from driving local RPCs.
|
||||||
|
package localaccess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AllowedOrigin(raw string) bool {
|
||||||
|
if raw == "" {
|
||||||
|
return true
|
||||||
|
} // Native Cursor/CLI requests do not send Origin.
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || u.User != nil || u.Hostname() == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "vscode-file", "vscode-webview", "wails":
|
||||||
|
return true
|
||||||
|
case "http", "https":
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host := strings.ToLower(u.Hostname())
|
||||||
|
if host == "localhost" || host == "wails.localhost" || net.ParseIP(host).IsLoopback() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, domain := range []string{"cursor.sh", "cursor.com"} {
|
||||||
|
if host == domain || strings.HasSuffix(host, "."+domain) {
|
||||||
|
return u.Scheme == "https"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !AllowedOrigin(r.Header.Get("Origin")) {
|
||||||
|
http.Error(w, "untrusted browser origin", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
26
internal/localaccess/origin_test.go
Normal file
26
internal/localaccess/origin_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package localaccess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNativeAndCursorOriginsAllowedUnrelatedPagesRejected(t *testing.T) {
|
||||||
|
for _, origin := range []string{"", "vscode-file://vscode-app", "vscode-webview://fixture", "wails://wails", "http://127.0.0.1:9245", "https://api2.cursor.sh", "https://www.cursor.com"} {
|
||||||
|
if !AllowedOrigin(origin) {
|
||||||
|
t.Errorf("rejected %q", origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, origin := range []string{"null", "https://evil.invalid", "https://api2.cursor.sh.evil.invalid", "https://api2.cursor.sh@evil.invalid", "file:///tmp/evil.html"} {
|
||||||
|
called := false
|
||||||
|
handler := Wrap(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
||||||
|
r := httptest.NewRequest(http.MethodPost, "http://127.0.0.1/rpc", nil)
|
||||||
|
r.Header.Set("Origin", origin)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
if w.Code != 403 || called {
|
||||||
|
t.Errorf("origin %q reached backend", origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cursor/internal/appdata"
|
"cursor/internal/appdata"
|
||||||
|
"cursor/internal/securefile"
|
||||||
|
|
||||||
"github.com/lmittmann/tint"
|
"github.com/lmittmann/tint"
|
||||||
"github.com/mattn/go-colorable"
|
"github.com/mattn/go-colorable"
|
||||||
@@ -50,7 +51,7 @@ func Init() {
|
|||||||
if len(handlers) > 1 {
|
if len(handlers) > 1 {
|
||||||
handler = &multiHandler{handlers: handlers}
|
handler = &multiHandler{handlers: handlers}
|
||||||
}
|
}
|
||||||
slog.SetDefault(slog.New(handler))
|
slog.SetDefault(slog.New(redactingHandler{handler}))
|
||||||
stdlog.SetFlags(0)
|
stdlog.SetFlags(0)
|
||||||
if logFilePath != "" {
|
if logFilePath != "" {
|
||||||
slog.Info("应用日志已写入文件", "path", logFilePath, "pid", os.Getpid())
|
slog.Info("应用日志已写入文件", "path", logFilePath, "pid", os.Getpid())
|
||||||
@@ -180,10 +181,14 @@ func (writer *lineWindowFileWriter) Write(payload []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (writer *lineWindowFileWriter) openLocked() error {
|
func (writer *lineWindowFileWriter) openLocked() error {
|
||||||
file, err := os.OpenFile(writer.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
file, err := os.OpenFile(writer.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := securefile.Protect(writer.path); err != nil {
|
||||||
|
file.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
writer.file = file
|
writer.file = file
|
||||||
logFile = file
|
logFile = file
|
||||||
return nil
|
return nil
|
||||||
@@ -204,7 +209,7 @@ func (writer *lineWindowFileWriter) trimToLastLinesLocked(targetLines int) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trimmed, lineCount := lastLinesBytes(payload, targetLines)
|
trimmed, lineCount := lastLinesBytes(payload, targetLines)
|
||||||
if err := os.WriteFile(writer.path, trimmed, 0o644); err != nil {
|
if err := os.WriteFile(writer.path, trimmed, 0o600); err != nil {
|
||||||
if reopenErr := writer.openLocked(); reopenErr != nil {
|
if reopenErr := writer.openLocked(); reopenErr != nil {
|
||||||
return errors.Join(err, reopenErr)
|
return errors.Join(err, reopenErr)
|
||||||
}
|
}
|
||||||
|
|||||||
85
internal/logger/redact.go
Normal file
85
internal/logger/redact.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var secretRegistry = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
values map[string]struct{}
|
||||||
|
}{values: map[string]struct{}{}}
|
||||||
|
var bearerPattern = regexp.MustCompile(`(?i)Bearer\s+[^\s"'<>]+`)
|
||||||
|
var credentialPattern = regexp.MustCompile(`(?i)((?:api[_-]?key|token|authorization|cookie|password|secret)["']?\s*[:=]\s*["']?)[^"'\s,;}]+`)
|
||||||
|
|
||||||
|
func RegisterSecrets(values ...string) {
|
||||||
|
secretRegistry.Lock()
|
||||||
|
defer secretRegistry.Unlock()
|
||||||
|
for _, v := range values {
|
||||||
|
if v != "" {
|
||||||
|
secretRegistry.values[v] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func redact(s string) string {
|
||||||
|
secretRegistry.RLock()
|
||||||
|
values := make([]string, 0, len(secretRegistry.values))
|
||||||
|
for value := range secretRegistry.values {
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
secretRegistry.RUnlock()
|
||||||
|
sort.Slice(values, func(i, j int) bool { return len(values[i]) > len(values[j]) })
|
||||||
|
for _, value := range values {
|
||||||
|
s = strings.ReplaceAll(s, value, "[redacted]")
|
||||||
|
}
|
||||||
|
s = bearerPattern.ReplaceAllString(s, "Bearer [redacted]")
|
||||||
|
return credentialPattern.ReplaceAllString(s, "${1}[redacted]")
|
||||||
|
}
|
||||||
|
|
||||||
|
type redactingHandler struct{ next slog.Handler }
|
||||||
|
|
||||||
|
func (h redactingHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
|
return h.next.Enabled(ctx, level)
|
||||||
|
}
|
||||||
|
func redactAttr(a slog.Attr) slog.Attr {
|
||||||
|
key := strings.ToLower(a.Key)
|
||||||
|
for _, part := range []string{"apikey", "api_key", "token", "password", "secret", "authorization", "cookie", "payload", "headers", "body", "config"} {
|
||||||
|
if strings.Contains(key, part) {
|
||||||
|
return slog.String(a.Key, "[redacted]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v := a.Value.Resolve()
|
||||||
|
switch v.Kind() {
|
||||||
|
case slog.KindString:
|
||||||
|
a.Value = slog.StringValue(redact(v.String()))
|
||||||
|
case slog.KindAny:
|
||||||
|
a.Value = slog.StringValue(redact(fmt.Sprint(v.Any())))
|
||||||
|
case slog.KindGroup:
|
||||||
|
attrs := v.Group()
|
||||||
|
for i := range attrs {
|
||||||
|
attrs[i] = redactAttr(attrs[i])
|
||||||
|
}
|
||||||
|
a.Value = slog.GroupValue(attrs...)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
func (h redactingHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
next := slog.NewRecord(r.Time, r.Level, redact(r.Message), r.PC)
|
||||||
|
r.Attrs(func(a slog.Attr) bool { next.AddAttrs(redactAttr(a)); return true })
|
||||||
|
return h.next.Handle(ctx, next)
|
||||||
|
}
|
||||||
|
func (h redactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
copy := append([]slog.Attr(nil), attrs...)
|
||||||
|
for i := range copy {
|
||||||
|
copy[i] = redactAttr(copy[i])
|
||||||
|
}
|
||||||
|
return redactingHandler{h.next.WithAttrs(copy)}
|
||||||
|
}
|
||||||
|
func (h redactingHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return redactingHandler{h.next.WithGroup(name)}
|
||||||
|
}
|
||||||
21
internal/logger/redact_test.go
Normal file
21
internal/logger/redact_test.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretsRedactedInMessagesErrorsAttributesAndGroups(t *testing.T) {
|
||||||
|
RegisterSecrets("SENTINEL_PRIVATE_KEY")
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
log := slog.New(redactingHandler{slog.NewJSONHandler(&buffer, nil)})
|
||||||
|
log.With("token", "BOUND_SECRET").Info("provider echoed SENTINEL_PRIVATE_KEY", "error", errors.New("Authorization: Bearer UNKNOWN_SECRET"), "payload", map[string]string{"config": "MAP_SECRET"}, "group", slog.GroupValue(slog.String("apiKey", "GROUP_SECRET")))
|
||||||
|
for _, secret := range []string{"SENTINEL_PRIVATE_KEY", "BOUND_SECRET", "UNKNOWN_SECRET", "MAP_SECRET", "GROUP_SECRET"} {
|
||||||
|
if strings.Contains(buffer.String(), secret) {
|
||||||
|
t.Fatalf("leaked secret %s", secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cursor/internal/certs"
|
"cursor/internal/certs"
|
||||||
|
"cursor/internal/localaccess"
|
||||||
"cursor/internal/logger"
|
"cursor/internal/logger"
|
||||||
"cursor/internal/netproxy"
|
"cursor/internal/netproxy"
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ const (
|
|||||||
|
|
||||||
// ProxyServer 定义了当前模块中的 ProxyServer 类型。
|
// ProxyServer 定义了当前模块中的 ProxyServer 类型。
|
||||||
type ProxyServer struct {
|
type ProxyServer struct {
|
||||||
|
lifecycleMu sync.Mutex
|
||||||
// addr 表示当前声明中的 addr。
|
// addr 表示当前声明中的 addr。
|
||||||
addr string
|
addr string
|
||||||
// baseURL 表示当前声明中的 baseURL。
|
// baseURL 表示当前声明中的 baseURL。
|
||||||
@@ -264,6 +266,8 @@ func (s *ProxyServer) ListenAndServe() error {
|
|||||||
|
|
||||||
// Start 用于处理与 Start 相关的逻辑。
|
// Start 用于处理与 Start 相关的逻辑。
|
||||||
func (s *ProxyServer) Start() error {
|
func (s *ProxyServer) Start() error {
|
||||||
|
s.lifecycleMu.Lock()
|
||||||
|
defer s.lifecycleMu.Unlock()
|
||||||
s.runMu.Lock()
|
s.runMu.Lock()
|
||||||
defer s.runMu.Unlock()
|
defer s.runMu.Unlock()
|
||||||
if s.httpServer != nil {
|
if s.httpServer != nil {
|
||||||
@@ -308,15 +312,24 @@ func (s *ProxyServer) Start() error {
|
|||||||
|
|
||||||
// Stop 用于处理与 Stop 相关的逻辑。
|
// Stop 用于处理与 Stop 相关的逻辑。
|
||||||
func (s *ProxyServer) Stop(ctx context.Context) error {
|
func (s *ProxyServer) Stop(ctx context.Context) error {
|
||||||
s.runMu.Lock()
|
s.lifecycleMu.Lock()
|
||||||
httpServer := s.httpServer
|
defer s.lifecycleMu.Unlock()
|
||||||
if httpServer == nil {
|
s.runMu.RLock()
|
||||||
s.runMu.Unlock()
|
server := s.httpServer
|
||||||
|
s.runMu.RUnlock()
|
||||||
|
if server == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
err := server.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
_ = server.Close()
|
||||||
|
}
|
||||||
|
s.runMu.Lock()
|
||||||
|
if s.httpServer == server {
|
||||||
s.httpServer = nil
|
s.httpServer = nil
|
||||||
|
}
|
||||||
s.runMu.Unlock()
|
s.runMu.Unlock()
|
||||||
return httpServer.Shutdown(ctx)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRunning 用于处理与 IsRunning 相关的逻辑。
|
// IsRunning 用于处理与 IsRunning 相关的逻辑。
|
||||||
@@ -706,6 +719,9 @@ func shouldHandleLocalCORSPreflight(req *http.Request) bool {
|
|||||||
|
|
||||||
// buildLocalCORSPreflightResponse 用于处理与 buildLocalCORSPreflightResponse 相关的逻辑。
|
// buildLocalCORSPreflightResponse 用于处理与 buildLocalCORSPreflightResponse 相关的逻辑。
|
||||||
func buildLocalCORSPreflightResponse(req *http.Request) *http.Response {
|
func buildLocalCORSPreflightResponse(req *http.Request) *http.Response {
|
||||||
|
if req != nil && !localaccess.AllowedOrigin(req.Header.Get("Origin")) {
|
||||||
|
return &http.Response{StatusCode: http.StatusForbidden, Status: "403 Forbidden", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("untrusted browser origin")), Request: req}
|
||||||
|
}
|
||||||
allowOrigin := "*"
|
allowOrigin := "*"
|
||||||
if req != nil {
|
if req != nil {
|
||||||
origin := strings.TrimSpace(req.Header.Get("Origin"))
|
origin := strings.TrimSpace(req.Header.Get("Origin"))
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ func NormalizeBaseURL(raw string) (string, error) {
|
|||||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||||
return "", fmt.Errorf("模型适配器 baseURL 仅支持 http 或 https")
|
return "", fmt.Errorf("模型适配器 baseURL 仅支持 http 或 https")
|
||||||
}
|
}
|
||||||
|
if parsed.User != nil {
|
||||||
|
return "", fmt.Errorf("模型适配器 baseURL 不允许内嵌凭据,请使用访问密钥或自定义请求头")
|
||||||
|
}
|
||||||
if strings.TrimSpace(parsed.Host) == "" {
|
if strings.TrimSpace(parsed.Host) == "" {
|
||||||
return "", fmt.Errorf("模型适配器 baseURL 缺少主机名")
|
return "", fmt.Errorf("模型适配器 baseURL 缺少主机名")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ func InstallDefaultTransport() {
|
|||||||
// NewHTTPClient creates an HTTP client that follows environment and OS proxy
|
// NewHTTPClient creates an HTTP client that follows environment and OS proxy
|
||||||
// settings while preserving the caller's timeout choice.
|
// settings while preserving the caller's timeout choice.
|
||||||
func NewHTTPClient(timeout time.Duration) *http.Client {
|
func NewHTTPClient(timeout time.Duration) *http.Client {
|
||||||
return &http.Client{
|
return CredentialSafeClient(&http.Client{
|
||||||
Transport: NewTransport(nil),
|
Transport: NewTransport(nil),
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTransport clones the given transport and installs proxy resolution on it.
|
// NewTransport clones the given transport and installs proxy resolution on it.
|
||||||
|
|||||||
43
internal/netproxy/redirect.go
Normal file
43
internal/netproxy/redirect.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package netproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func origin(u *url.URL) string {
|
||||||
|
port := u.Port()
|
||||||
|
if port == "" {
|
||||||
|
if strings.EqualFold(u.Scheme, "https") {
|
||||||
|
port = "443"
|
||||||
|
} else {
|
||||||
|
port = "80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ToLower(u.Scheme) + "://" + strings.ToLower(u.Hostname()) + ":" + port
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialSafeClient retains client settings, while never forwarding custom
|
||||||
|
// authentication headers or POST bodies to a different origin via redirects.
|
||||||
|
func CredentialSafeClient(client *http.Client) *http.Client {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
next := *client
|
||||||
|
previous := next.CheckRedirect
|
||||||
|
next.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= 10 {
|
||||||
|
return fmt.Errorf("too many redirects")
|
||||||
|
}
|
||||||
|
if len(via) > 0 && origin(req.URL) != origin(via[0].URL) {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
if previous != nil {
|
||||||
|
return previous(req, via)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &next
|
||||||
|
}
|
||||||
67
internal/netproxy/redirect_test.go
Normal file
67
internal/netproxy/redirect_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package netproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCrossOriginRedirectNeverReceivesCustomAuthentication(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true }))
|
||||||
|
defer target.Close()
|
||||||
|
redirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect)
|
||||||
|
}))
|
||||||
|
defer redirect.Close()
|
||||||
|
client := CredentialSafeClient(redirect.Client())
|
||||||
|
r, _ := http.NewRequest(http.MethodPost, redirect.URL, strings.NewReader("private body"))
|
||||||
|
r.Header.Set("x-api-key", "sentinel")
|
||||||
|
r.Header.Set("Authorization", "Bearer sentinel")
|
||||||
|
resp, err := client.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if called || resp.StatusCode != 307 {
|
||||||
|
t.Fatal("credentials crossed origin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSameOriginRedirectAndCancellation(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.Redirect(w, r, "/ok", 307)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Header.Get("x-api-key") != "sentinel" {
|
||||||
|
t.Error("same-origin credential lost")
|
||||||
|
}
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
client := CredentialSafeClient(server.Client())
|
||||||
|
r, _ := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||||
|
r.Header.Set("x-api-key", "sentinel")
|
||||||
|
resp, err := client.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != 204 {
|
||||||
|
t.Fatal(resp.StatusCode)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
r, _ = http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
|
||||||
|
if _, err = client.Do(r); err == nil {
|
||||||
|
t.Fatal("cancellation ignored")
|
||||||
|
}
|
||||||
|
client.Timeout = time.Nanosecond
|
||||||
|
if client.Timeout == 0 {
|
||||||
|
t.Fatal("timeout setting lost")
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/securefile/file.go
Normal file
58
internal/securefile/file.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Package securefile provides private, durable files for application secrets.
|
||||||
|
package securefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrivateDir(path string) error {
|
||||||
|
if err := os.MkdirAll(path, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return protect(path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Protect(path string) error { return protect(path, false) }
|
||||||
|
|
||||||
|
func Write(path string, data []byte) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(filepath.Dir(path), ".local-cursor-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temp := f.Name()
|
||||||
|
defer os.Remove(temp)
|
||||||
|
if err = protect(temp, false); err == nil {
|
||||||
|
_, err = f.Write(data)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
closeErr := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
return os.Rename(temp, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock is released by the OS when the process exits, including after a crash.
|
||||||
|
func Lock(path string) (func(), error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = Protect(path); err == nil {
|
||||||
|
err = lockFile(f)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func() { unlockFile(f); f.Close() }, nil
|
||||||
|
}
|
||||||
17
internal/securefile/file_unix.go
Normal file
17
internal/securefile/file_unix.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package securefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func protect(path string, directory bool) error {
|
||||||
|
if directory {
|
||||||
|
return os.Chmod(path, 0700)
|
||||||
|
}
|
||||||
|
return os.Chmod(path, 0600)
|
||||||
|
}
|
||||||
|
func lockFile(f *os.File) error { return unix.Flock(int(f.Fd()), unix.LOCK_EX) }
|
||||||
|
func unlockFile(f *os.File) { _ = unix.Flock(int(f.Fd()), unix.LOCK_UN) }
|
||||||
34
internal/securefile/file_windows.go
Normal file
34
internal/securefile/file_windows.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package securefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func protect(path string, directory bool) error {
|
||||||
|
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inherit := ""
|
||||||
|
if directory {
|
||||||
|
inherit = "OICI"
|
||||||
|
}
|
||||||
|
sd, err := windows.SecurityDescriptorFromString("D:P(A;" + inherit + ";FA;;;SY)(A;" + inherit + ";FA;;;" + user.User.Sid.String() + ")")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
acl, _, err := sd.DACL()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, nil, nil, acl, nil)
|
||||||
|
}
|
||||||
|
func lockFile(f *os.File) error {
|
||||||
|
return windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &windows.Overlapped{})
|
||||||
|
}
|
||||||
|
func unlockFile(f *os.File) {
|
||||||
|
_ = windows.UnlockFileEx(windows.Handle(f.Fd()), 0, 1, 0, &windows.Overlapped{})
|
||||||
|
}
|
||||||
113
internal/skills/safety.go
Normal file
113
internal/skills/safety.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package skills
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"cursor/internal/appdata"
|
||||||
|
"cursor/internal/securefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
var operationMu sync.Mutex
|
||||||
|
|
||||||
|
func checkManagedPaths(project, name string) error {
|
||||||
|
paths := []string{filepath.Join(project, ".cursor"), skillsDir(project), skillTargetDir(project, name), projectCommandsDir(project)}
|
||||||
|
for _, file := range commandFileNames(name) {
|
||||||
|
paths = append(paths, filepath.Join(projectCommandsDir(project), file))
|
||||||
|
}
|
||||||
|
for _, entry := range paths {
|
||||||
|
info, err := os.Lstat(entry)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return fmt.Errorf("安全保护:拒绝通过符号链接修改 %s", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target := skillTargetDir(project, name)
|
||||||
|
if _, err := os.Stat(target); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.Type()&os.ModeSymlink != 0 {
|
||||||
|
return fmt.Errorf("安全保护:skill 内含符号链接 %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupBeforeReplace(target string, next []byte) error {
|
||||||
|
previous, err := os.ReadFile(target)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(previous, next) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
id := fmt.Sprintf("%x", sha256.Sum256([]byte(target)))
|
||||||
|
dir := filepath.Join(appdata.DataRootPath(), "skill-backups", id[:16])
|
||||||
|
if err = securefile.PrivateDir(dir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := os.CreateTemp(dir, filepath.Base(target)+"-*.bak")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name := file.Name()
|
||||||
|
if err = file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return securefile.Write(name, previous)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureNoCustomSkillFiles(project, name string) error {
|
||||||
|
allowed := map[string]bool{}
|
||||||
|
err := fs.WalkDir(builtinFS, path.Join(builtinRoot, name), func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
rel := strings.TrimPrefix(p, path.Join(builtinRoot, name)+"/")
|
||||||
|
allowed[rel] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, file := range retiredSkillFiles[name] {
|
||||||
|
allowed[file] = true
|
||||||
|
}
|
||||||
|
target := skillTargetDir(project, name)
|
||||||
|
if _, err = os.Stat(target); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return filepath.WalkDir(target, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
rel, _ := filepath.Rel(target, p)
|
||||||
|
if !allowed[filepath.ToSlash(rel)] {
|
||||||
|
return fmt.Errorf("skill 含自建文件 %s,已保留;请先备份后手动移除", strings.TrimPrefix(p, target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
50
internal/skills/safety_test.go
Normal file
50
internal/skills/safety_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package skills
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUninstallRejectsUnknownSkillAndPreservesCustomFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
custom := filepath.Join(dir, ".cursor", "skills", "user-owned")
|
||||||
|
_ = os.MkdirAll(custom, 0700)
|
||||||
|
file := filepath.Join(custom, "SKILL.md")
|
||||||
|
_ = os.WriteFile(file, []byte("user data"), 0600)
|
||||||
|
if err := Uninstall(dir, "user-owned"); err == nil {
|
||||||
|
t.Fatal("unknown skill accepted")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(file); err != nil {
|
||||||
|
t.Fatal("custom skill deleted")
|
||||||
|
}
|
||||||
|
if err := Install(dir, "fd-engineering"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
extra := filepath.Join(skillTargetDir(dir, "fd-engineering"), "my-notes.md")
|
||||||
|
_ = os.WriteFile(extra, []byte("notes"), 0600)
|
||||||
|
if err := Uninstall(dir, "fd-engineering"); err == nil {
|
||||||
|
t.Fatal("custom files silently deleted")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(extra); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallPreservesModifiedCommand(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := Install(dir, "fd-engineering"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
command := filepath.Join(projectCommandsDir(dir), "FD-plan.md")
|
||||||
|
if err := os.WriteFile(command, []byte("custom command"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Uninstall(dir, "fd-engineering"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(command)
|
||||||
|
if err != nil || string(data) != "custom command" {
|
||||||
|
t.Fatal("modified command deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
package skills
|
package skills
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -129,6 +130,16 @@ func Exists(name string) bool {
|
|||||||
// 并将其 commands/ 模板分发到项目 .cursor/commands/。
|
// 并将其 commands/ 模板分发到项目 .cursor/commands/。
|
||||||
// 目标已存在时同步为最新内置内容,并清理已退役的内置文件。
|
// 目标已存在时同步为最新内置内容,并清理已退役的内置文件。
|
||||||
func Install(projectPath string, name string) error {
|
func Install(projectPath string, name string) error {
|
||||||
|
name = sanitizeSkillName(name)
|
||||||
|
projectPath = strings.TrimSpace(projectPath)
|
||||||
|
operationMu.Lock()
|
||||||
|
defer operationMu.Unlock()
|
||||||
|
if !Exists(name) {
|
||||||
|
return fmt.Errorf("未知的内置 skill")
|
||||||
|
}
|
||||||
|
if err := checkManagedPaths(projectPath, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
name = sanitizeSkillName(name)
|
name = sanitizeSkillName(name)
|
||||||
if name == "" || !Exists(name) {
|
if name == "" || !Exists(name) {
|
||||||
return fmt.Errorf("内置 skill 不存在: %s", name)
|
return fmt.Errorf("内置 skill 不存在: %s", name)
|
||||||
@@ -170,6 +181,9 @@ func installCommands(projectPath string, name string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("读取命令模板失败: %w", err)
|
return fmt.Errorf("读取命令模板失败: %w", err)
|
||||||
}
|
}
|
||||||
|
if err := backupBeforeReplace(filepath.Join(commandsDir, fileName), data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := os.WriteFile(filepath.Join(commandsDir, fileName), data, 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(commandsDir, fileName), data, 0o644); err != nil {
|
||||||
return fmt.Errorf("写入命令文件失败: %w", err)
|
return fmt.Errorf("写入命令文件失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -179,6 +193,19 @@ func installCommands(projectPath string, name string) error {
|
|||||||
|
|
||||||
// Uninstall 移除项目内的 skill 目录与随附的命令文件。不存在时幂等成功。
|
// Uninstall 移除项目内的 skill 目录与随附的命令文件。不存在时幂等成功。
|
||||||
func Uninstall(projectPath string, name string) error {
|
func Uninstall(projectPath string, name string) error {
|
||||||
|
name = sanitizeSkillName(name)
|
||||||
|
projectPath = strings.TrimSpace(projectPath)
|
||||||
|
operationMu.Lock()
|
||||||
|
defer operationMu.Unlock()
|
||||||
|
if !Exists(name) {
|
||||||
|
return fmt.Errorf("未知的内置 skill")
|
||||||
|
}
|
||||||
|
if err := checkManagedPaths(projectPath, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureNoCustomSkillFiles(projectPath, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
name = sanitizeSkillName(name)
|
name = sanitizeSkillName(name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("skill 名称不能为空")
|
return fmt.Errorf("skill 名称不能为空")
|
||||||
@@ -196,6 +223,14 @@ func Uninstall(projectPath string, name string) error {
|
|||||||
// 只删除内置清单中的命令文件,不碰用户自建命令。
|
// 只删除内置清单中的命令文件,不碰用户自建命令。
|
||||||
commandsDir := projectCommandsDir(projectPath)
|
commandsDir := projectCommandsDir(projectPath)
|
||||||
for _, fileName := range commandFileNames(name) {
|
for _, fileName := range commandFileNames(name) {
|
||||||
|
expected, _ := builtinFS.ReadFile(path.Join(builtinRoot, name, commandsDirName, fileName))
|
||||||
|
current, readErr := os.ReadFile(filepath.Join(commandsDir, fileName))
|
||||||
|
if readErr == nil && !bytes.Equal(current, expected) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if readErr != nil && !os.IsNotExist(readErr) {
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
if err := os.Remove(filepath.Join(commandsDir, fileName)); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(filepath.Join(commandsDir, fileName)); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("移除命令文件失败: %w", err)
|
return fmt.Errorf("移除命令文件失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -258,6 +293,9 @@ func copyEmbeddedDir(embedDir string, targetDir string, skipDir string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := backupBeforeReplace(target, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return os.WriteFile(target, data, 0o644)
|
return os.WriteFile(target, data, 0o644)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
13
main.go
13
main.go
@@ -2,8 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cursor/internal/app"
|
"cursor/internal/app"
|
||||||
|
"cursor/internal/buildinfo"
|
||||||
"cursor/internal/logger"
|
"cursor/internal/logger"
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,8 +19,19 @@ var appIcon []byte
|
|||||||
var trayIcon []byte
|
var trayIcon []byte
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
disableAutoStart := false
|
||||||
|
for _, arg := range os.Args[1:] {
|
||||||
|
if arg == "--version" {
|
||||||
|
fmt.Println(buildinfo.CurrentVersion())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if arg == "--no-auto-start" {
|
||||||
|
disableAutoStart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.Init()
|
logger.Init()
|
||||||
if err := app.Run(app.EmbeddedResources{
|
if err := app.Run(app.EmbeddedResources{
|
||||||
|
DisableAutoStart: disableAutoStart,
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
AppIcon: appIcon,
|
AppIcon: appIcon,
|
||||||
TrayIcon: trayIcon,
|
TrayIcon: trayIcon,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# Local-Cursor
|
# Local-Cursor v1.1.9
|
||||||
|
|
||||||
- 个人使用版本
|
- 改用本机独立 CA,首次升级需重新授权信任;恢复 CLI TLS 验证并支持中文、空格和引号路径。
|
||||||
- 移除广告系统
|
- 修复配置端口丢失、多窗口覆盖、损坏配置被删除、最近项目记录字段丢失和异常关闭后端口占用。
|
||||||
- 移除自动更新
|
- 移除日志与浏览器缓存中的敏感配置;保护本机私钥和日志,限制无关网站访问本地 RPC。
|
||||||
- 默认端口改为 28090/28080,避免与原版冲突
|
- 修复 Protobuf 锁复制和用量统计测试,完善模型请求头、流式响应、取消及重定向处理。
|
||||||
- Tab 补全服务改为可配置
|
- 独立 Tab 服务增加必填客户端令牌;远程部署需同步配置 clientToken/tabServerToken。
|
||||||
|
- 加固 Skills 文件操作,升级 Go 和前端安全依赖,固定可重现构建工具链与版本资源。
|
||||||
|
- 仅提供 Windows x64 便携版。详细测试、平台限制与升级说明见 docs/review-v1.1.9.md。
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ var releaseAssets = []assetSpec{
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
exitf("usage: go run ./scripts/release <version|notes|manifest> [flags]")
|
exitf("usage: go run ./scripts/release <version|notes|manifest|sync-metadata> [flags]")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
case "sync-metadata":
|
||||||
|
runMetadata(os.Args[2:])
|
||||||
case "version":
|
case "version":
|
||||||
runVersion(os.Args[2:])
|
runVersion(os.Args[2:])
|
||||||
case "notes":
|
case "notes":
|
||||||
|
|||||||
65
scripts/release/metadata.go
Normal file
65
scripts/release/metadata.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type metadataRule struct{ path, pattern, version string }
|
||||||
|
|
||||||
|
// syncMetadata updates version fields only. Regenerating Wails build templates
|
||||||
|
// during a release used to overwrite customized platform packaging tasks.
|
||||||
|
func syncMetadata(root, version string) error {
|
||||||
|
if !regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$`).MatchString(version) {
|
||||||
|
return fmt.Errorf("invalid release version")
|
||||||
|
}
|
||||||
|
core := strings.SplitN(version, "-", 2)[0]
|
||||||
|
rules := []metadataRule{
|
||||||
|
{"build/windows/info.json", `("(?:file_version|product_version|FileVersion|ProductVersion)"\s*:\s*")[^"]*(")`, core},
|
||||||
|
{"build/windows/wails.exe.manifest", `(<assemblyIdentity[^>]*version=")[^"]*(")`, core + ".0"},
|
||||||
|
{"build/windows/nsis/wails_tools.nsh", `(!define INFO_PRODUCTVERSION ")[^"]*(")`, core},
|
||||||
|
{"build/darwin/Info.plist", `(<key>CFBundle(?:ShortVersionString|Version)</key>\s*<string>)[^<]*(</string>)`, version},
|
||||||
|
{"build/darwin/Info.dev.plist", `(<key>CFBundle(?:ShortVersionString|Version)</key>\s*<string>)[^<]*(</string>)`, version},
|
||||||
|
{"build/linux/nfpm/nfpm.yaml", `(?m)(^version:\s*")[^"]*(")`, version},
|
||||||
|
}
|
||||||
|
updates := make(map[string][]byte)
|
||||||
|
for _, rule := range rules {
|
||||||
|
file := filepath.Join(root, filepath.FromSlash(rule.path))
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pattern := regexp.MustCompile(rule.pattern)
|
||||||
|
if !pattern.Match(data) {
|
||||||
|
return fmt.Errorf("missing version field in %s", rule.path)
|
||||||
|
}
|
||||||
|
updates[file] = pattern.ReplaceAll(data, []byte("${1}"+rule.version+"${2}"))
|
||||||
|
}
|
||||||
|
for file, data := range updates {
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(file, data, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMetadata(args []string) {
|
||||||
|
flags := flag.NewFlagSet("sync-metadata", flag.ExitOnError)
|
||||||
|
root := flags.String("root", ".", "project root")
|
||||||
|
_ = flags.Parse(args)
|
||||||
|
version, err := readVersion(filepath.Join(*root, "build", "config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
exitErr(err)
|
||||||
|
}
|
||||||
|
if err = syncMetadata(*root, version); err != nil {
|
||||||
|
exitErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
scripts/release/metadata_test.go
Normal file
43
scripts/release/metadata_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetadataSyncPreservesCustomBuildFilesAndContent(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
files := []string{"build/windows/info.json", "build/windows/wails.exe.manifest", "build/windows/nsis/wails_tools.nsh", "build/darwin/Info.plist", "build/darwin/Info.dev.plist", "build/linux/nfpm/nfpm.yaml"}
|
||||||
|
for _, file := range files {
|
||||||
|
data, err := os.ReadFile(filepath.Join("..", "..", filepath.FromSlash(file)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
target := filepath.Join(root, filepath.FromSlash(file))
|
||||||
|
_ = os.MkdirAll(filepath.Dir(target), 0700)
|
||||||
|
if err = os.WriteFile(target, data, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task := filepath.Join(root, "build", "windows", "Taskfile.yml")
|
||||||
|
_ = os.WriteFile(task, []byte("custom packaging must stay"), 0600)
|
||||||
|
if err := syncMetadata(root, "9.8.7"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
data, _ := os.ReadFile(filepath.Join(root, filepath.FromSlash(file)))
|
||||||
|
if !strings.Contains(string(data), "9.8.7") {
|
||||||
|
t.Fatalf("version not updated: %s", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(task)
|
||||||
|
if string(data) != "custom packaging must stay" {
|
||||||
|
t.Fatal("custom task overwritten")
|
||||||
|
}
|
||||||
|
data, _ = os.ReadFile(filepath.Join(root, "build", "windows", "info.json"))
|
||||||
|
if !strings.Contains(string(data), "Local-Cursor") {
|
||||||
|
t.Fatal("product metadata lost")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user