diff --git a/.env.example b/.env.example index 48b1dc5b8..2eb210a50 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ CANVAS_REGISTRATION_ENABLED=false CANVAS_ALLOW_PRIVATE_UPSTREAMS=false # 多个主机使用英文逗号分隔,例如:gateway.internal,192.168.1.10 CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS= -CANVAS_CORS_ORIGINS=http://localhost:3000 +CANVAS_CORS_ORIGINS=http://localhost:7380 # 服务器对模型上游可访问的地址:本地存储的短时签名链接(Agent 看图、生成任务参考图)由它拼出, # 内网部署还要把该主机放进 CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS。留空则本地存储无法签出链接。 CANVAS_PUBLIC_BASE_URL= @@ -37,7 +37,8 @@ CANVAS_UPDATER_SOCKET_DIR=/run/open-ai-canvas-updater # REDIS_URL=redis://redis:6379/0 # CANVAS_POSTGRES_DATA_PATH=./postgres-data # CANVAS_REDIS_DATA_PATH=./redis-data -# CANVAS_HTTP_PORT=3000 +# 宿主机访问端口;本项目使用 7380 以避免与同机其它工程(默认 3000)冲突 +# CANVAS_HTTP_PORT=7380 # 后端日志:info 级别不输出成功的查询请求与 Agent 流式事件;排障时临时改为 debug。 # CANVAS_LOG_LEVEL=info diff --git a/.gitignore b/.gitignore index 4c33f16c4..33df6fc8a 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,8 @@ camera_language_guide.md # Local backend executable; never commit build output. backend/cmd/server/server +# 本项目附加:在 backend/ 目录直接 go build 会产出同名 server 二进制(曾误提交 62.7MB)。 +/backend/server # One-off local probe scripts and their compiled output. backend/tmp_* # Keep the runtime in source control; its optional debug map is not deployed. diff --git a/AGENTS.md b/AGENTS.md index 88576265f..fac25062f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,3 +164,96 @@ cd web && bun run dev - Git 提交说明使用 `(): <业务模块> - <变更摘要>`,`type` 为 `feat|fix|refactor|perf|docs|test|build|ci|chore|revert`。 交付前至少检查:改动是否聚焦、调用方和类型是否同步、错误/权限/数据归属是否完整、必要文档是否同步、验证是否如实说明、是否留下密钥或本地数据。 + +--- + +# 本项目附加约定 + +以上第 1–9 章继承自上游 `ddcat-ai/open-ai-canvas`。以下为本仓库(`ttstd-infinite-canvas`)在底座之上叠加的约定;**与上游冲突时以本章为准**。 + +## A. 底座与上游同步 + +本仓库以 `ddcat-ai/open-ai-canvas` 为底座,通过 Git remote 保持可追溯的上游关系: + +- `upstream` → `https://github.com/ddcat-ai/open-ai-canvas.git`(开发机上另存有一份克隆,可作离线 fetch 源)。 +- 同步新功能:`git fetch upstream && git merge upstream/main`。冲突优先保留上游实现,本仓库的差异应集中在 B~D 三项。 +- **功能一律优先使用上游源码实现**,不要在本仓库另起一套。确需偏离上游时,先在本章登记原因再改代码。 +- `libtv.html` 与 `libtv-前端技术分析.md` 是竞品运行时快照与分析资料,属于本地参考,不要当作上游代码改动。 +- 分支:本仓库工作分支为 `main`,上游分支也是 `main`(位于 `upstream` remote);同步用 `git fetch upstream && git merge upstream/main`。 + +## B. 端口约定 + +**不要改回上游默认的 3000 / 8080**——同机上 `infinite-canvas`、`open-ai-canvas`、`open-canvas` 都占用 3000,会互相抢占。 + +| 场景 | 前端 | 后端 | +| --- | --- | --- | +| 本地开发(宿主机) | **7380** | **7381** | +| Docker(宿主机映射) | **7380** → 容器 3000 | 不暴露(容器内仍 8080) | + +容器内部端口保持上游原样(nginx `listen 3000`、后端 `:8080`),以减小与上游的差异面。改端口时必须同步: + +- `web/package.json` — `dev` / `start` 的 `--port` +- `web/vite.config.ts` — `apiProxyTarget` 默认值 +- `backend/cmd/server/main.go` — `CANVAS_BACKEND_ADDR` 默认值 +- `scripts/start-local.ps1` — 后端地址、代理目标、提示文案 +- `docker-compose.yml`、`docker-compose.local.yml` — 宿主机端口映射与 `CANVAS_CORS_ORIGINS` +- `.env.example` — `CANVAS_CORS_ORIGINS`、`CANVAS_HTTP_PORT` +- `README.md` 快速开始 + +`docker-compose.deploy.yml` / `docker-compose.server.yml` 与 `scripts/install-server*.sh` 面向独立服务器部署,保留上游的 3000 默认值,不做本地化改动。 + +`scripts/launch-canvas.ps1` 是上游维护者的个人脚本(硬编码 `D:\yingce\...`、`Administrator`、cloudflared 隧道),在本机不可用,**保持原样不要改**。 + +## C. 许可与商标红线 + +- 本仓库与上游均为 **MIT**。复制上游代码必须保留其版权声明与 `NOTICE`(声明 fork 自 `basketikun/infinite-canvas`,上游作者权利保留)。 +- **`tldraw` 许可非 MIT**:生产环境商用需购买 license key,否则必须保留 "Made with tldraw" 水印。上游已预留 `VITE_TLDRAW_LICENSE_KEY`(构建期)与 `tldraw_license_key`(运行时设置),对外商用前必须就此明确决策。 +- **不得新增他人 logo / 商标**:`web/public/icons/` 下的模型厂商图标、根 README 的赞助商与社群素材仅在上游语境下存在,对外分发前必须清理或替换为纯文字。 +- **禁止把带 GPL 组件的 ffmpeg 打进分发镜像**:服务端 ffmpeg 使用 LGPL 构建或由用户挂载(`CANVAS_FFMPEG_PATH`)。 +- 面向国内公众提供服务时,需处理 AI 生成内容标识、生成式 AI 服务备案与深度合成管理要求。 + +## D. 进程清理(强制) + +验收或调试结束后清理进程时,**只允许按精确 PID 操作**,严禁用命令名、路径片段或宽正则批量匹配后终止。 + +正确做法: + +1. `netstat -ano | Select-String 'LISTENING' | Select-String ':<端口>'` 拿到占用端口的 PID; +2. `Get-Process -Id ` 确认进程名与可执行文件路径确属本项目; +3. `Stop-Process -Id -Force`;需要连带子进程时用 `taskkill /PID /T /F`。 + +禁止的做法(已有实际事故): + +- ❌ `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'server\.exe' } | Stop-Process` + 曾匹配到 IDE 的 `embeddings-server.exe`,导致误杀开发环境进程。 +- ❌ `-match 'vite'` / `-match 'node'` / `-match 'concurrently'` / `-match 'go'` + 这些字符串同样会命中编辑器本体、语言服务器,以及同机上其它工程。 + +补充要求: + +- 需要批量清理时,**先只打印**匹配结果(PID、进程名、完整命令行)供人工确认,确认后另起一步执行终止;不要在同一条命令里「查完即杀」。 +- 无法确认归属的进程一律不动,交回用户处理。同机存在多个同类工程时尤其要区分(例如 `infinite-canvas/web/.../vite.js` 与本项目的 `vite.js`)。 +- 误杀非本项目进程时,必须立即如实告知用户,说明进程名、可能影响与恢复方式,不要轻描淡写。 + +## E. 上游同步与 `backend/server` 大文件 + +上游在 `9727656b`(2026-09-14)提交了 `backend/server` —— 一个 62.7 MB 的可执行构建产物(blob `cfba15a0`),且至今仍保留在 `upstream/main` 中。 + +本仓库曾用 `git filter-repo --path backend/server --invert-paths` 把它从全部历史移除,代价是本地历史与上游彻底断开血缘(不能用 `git merge`,只能「指定基线 + 快照提交」)。2026-10-13 起改为**以上游为基底、保留上游血缘**:`main` 重建为 `upstream/main@e70fe1b9` 的子提交(`d6d6eab5`,内容取当时工作树),随后正常合并 `upstream/main`。旧的「快照同步」规则随之作废。 + +现状与约束: + +- `main` 是 `upstream/main` 的后代,**可以正常 `git merge upstream/main`**。日常同步: + + ```powershell + git fetch upstream + git merge upstream/main # 常规三方合并;冲突优先保留上游实现,本仓库差异应集中在 B~D 三项 + git ls-files backend/server # 必须为空;若因上游改动而重新出现,执行 git rm backend/server 后再提交 + git push origin main + ``` + +- **`backend/server` 的 blob 常驻历史**(因为它在上游祖先里),克隆与推送体积会包含它;这是换取上游血缘的必然代价。它已被 `.gitignore` 的 `/backend/server` 忽略,也不在 `main` 的工作树与索引中(`git ls-files backend/server` 为空),因此本地不会被检出。上游将来清理该文件后,直接正常合并即可两全。 +- **不要再次运行 `git filter-repo` 或任何历史重写**:那会重新切断与上游的血缘,回到旧的快照同步模式。 +- 每次合并后检查 `git ls-files backend/server`;只有上游确实改动该文件时它才会重新进入索引,用 `git rm backend/server` 处理并提交,不要改回 `.gitignore` 的忽略规则。 +- 不要用 `git diff --binary` 生成差异补丁:二进制补丁会把该 blob 整段写进补丁文件(实测 43.8 MB)。 +- 迁移用的临时分支、bundle 以及 filter-repo 重写前的备份均已在本地清理;本地不再保留旧(filter-repo)历史,需要追溯只能看 `origin`(Gitea)或 `upstream`。 diff --git a/README.md b/README.md index ff6e3b63d..bc63cdb80 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ mkdir -p .local/project-workbench-debug .local/cache/go-build .local/cache/go-mo # 终端一:后端 cd backend -CANVAS_BACKEND_ADDR=127.0.0.1:8080 \ +CANVAS_BACKEND_ADDR=127.0.0.1:7381 \ CANVAS_BACKEND_DATA_DIR=../.local/project-workbench-debug \ GOCACHE=../.local/cache/go-build \ GOMODCACHE=../.local/cache/go-mod \ @@ -108,7 +108,9 @@ bun install --frozen-lockfile bun run dev ``` -打开 。首次使用时注册管理员账号,并在设置中配置模型渠道。前端默认将 `/api` 代理到 `http://127.0.0.1:8080`;如需修改代理目标,可设置 `VITE_API_PROXY_TARGET`。 +打开 。首次使用时注册管理员账号,并在设置中配置模型渠道。前端默认将 `/api` 代理到 `http://127.0.0.1:7381`;如需修改代理目标,可设置 `VITE_API_PROXY_TARGET`。 + +> **本项目端口约定**:本地开发使用前端 **7380** / 后端 **7381**,Docker 宿主机端口为 **7380**(容器内仍为上游默认的 3000/8080)。这样可与同机上的 `infinite-canvas`、`open-ai-canvas` 等工程并存。详见 [AGENTS.md](./AGENTS.md) 的「本项目附加约定」。 Windows PowerShell 用户可在仓库根目录执行: diff --git a/backend/cmd/migrate-channel-model-price-tiers/fork_env.go b/backend/cmd/migrate-channel-model-price-tiers/fork_env.go new file mode 100644 index 000000000..a595150b9 --- /dev/null +++ b/backend/cmd/migrate-channel-model-price-tiers/fork_env.go @@ -0,0 +1,8 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,仅让该命令启动时读取仓库根 .env。 +package main + +import "yingce/backend/internal/config" + +func init() { + config.LoadDotEnv() +} diff --git a/backend/cmd/migrate-logical-model-families/fork_env.go b/backend/cmd/migrate-logical-model-families/fork_env.go new file mode 100644 index 000000000..a595150b9 --- /dev/null +++ b/backend/cmd/migrate-logical-model-families/fork_env.go @@ -0,0 +1,8 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,仅让该命令启动时读取仓库根 .env。 +package main + +import "yingce/backend/internal/config" + +func init() { + config.LoadDotEnv() +} diff --git a/backend/cmd/migrate-schema/fork_env.go b/backend/cmd/migrate-schema/fork_env.go new file mode 100644 index 000000000..a595150b9 --- /dev/null +++ b/backend/cmd/migrate-schema/fork_env.go @@ -0,0 +1,8 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,仅让该命令启动时读取仓库根 .env。 +package main + +import "yingce/backend/internal/config" + +func init() { + config.LoadDotEnv() +} diff --git a/backend/cmd/migrate-sqlite-postgres/fork_env.go b/backend/cmd/migrate-sqlite-postgres/fork_env.go new file mode 100644 index 000000000..a595150b9 --- /dev/null +++ b/backend/cmd/migrate-sqlite-postgres/fork_env.go @@ -0,0 +1,8 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,仅让该命令启动时读取仓库根 .env。 +package main + +import "yingce/backend/internal/config" + +func init() { + config.LoadDotEnv() +} diff --git a/backend/cmd/reseed-logical-model-sources/fork_env.go b/backend/cmd/reseed-logical-model-sources/fork_env.go new file mode 100644 index 000000000..a595150b9 --- /dev/null +++ b/backend/cmd/reseed-logical-model-sources/fork_env.go @@ -0,0 +1,8 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,仅让该命令启动时读取仓库根 .env。 +package main + +import "yingce/backend/internal/config" + +func init() { + config.LoadDotEnv() +} diff --git a/backend/cmd/server/fork_env.go b/backend/cmd/server/fork_env.go new file mode 100644 index 000000000..ec876a78a --- /dev/null +++ b/backend/cmd/server/fork_env.go @@ -0,0 +1,28 @@ +// 本项目附加文件(fork 专属):不改上游 main.go,达到同样的启动行为。 +// 独立成一个文件后,上游对同目录 main.go 的任何改动都不会再与本文件冲突。 +package main + +import ( + "log" + "os" + "strings" + + "yingce/backend/internal/config" +) + +func init() { + config.LoadDotEnv() + + // 本地开发默认绑定 127.0.0.1:7381(见 AGENTS.md「本项目附加约定」)。 + // 显式配置优先,例如 Compose 注入的 CANVAS_BACKEND_ADDR=":8080" 不会被覆盖。 + if strings.TrimSpace(os.Getenv("CANVAS_BACKEND_ADDR")) == "" { + _ = os.Setenv("CANVAS_BACKEND_ADDR", "127.0.0.1:7381") + } + + // 启动诊断:确认 .env / 环境变量已被读到,避免误用空 DATABASE_URL 时的静默回退。 + dbURLState := "empty" + if strings.TrimSpace(os.Getenv("DATABASE_URL")) != "" { + dbURLState = "set" + } + log.Printf("fork-env: CANVAS_BACKEND_ADDR=%s, DATABASE_URL=%s", os.Getenv("CANVAS_BACKEND_ADDR"), dbURLState) +} diff --git a/backend/internal/config/dotenv.go b/backend/internal/config/dotenv.go new file mode 100644 index 000000000..b6be1731c --- /dev/null +++ b/backend/internal/config/dotenv.go @@ -0,0 +1,83 @@ +// Package config 提供轻量的运行时配置辅助能力。 +// +// 后端只从环境变量读取配置;为方便本地开发(不依赖启动脚本注入), +// 这里提供一个零依赖的 .env 加载器,供 server / migrate-schema 等命令共用。 +package config + +import ( + "bufio" + "log" + "os" + "path/filepath" + "strings" +) + +// LoadDotEnv 从若干候选路径读取 .env 并注入进程环境变量。 +// +// 规则: +// - 已存在的环境变量优先,不会被覆盖(Docker/脚本注入的值始终生效); +// - 跳过空行与 # 注释,兼容 export 前缀与首尾引号; +// - 找不到 .env 时静默跳过,不影响生产部署。 +func LoadDotEnv() { + path := findDotEnvPath() + if path == "" { + return + } + file, err := os.Open(path) + if err != nil { + return + } + defer file.Close() + + log.Printf("已从 %s 加载 .env 配置(已存在的环境变量优先,不覆盖)", path) + + scanner := bufio.NewScanner(file) + // 连接串可能较长,放宽单行上限 + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for scanner.Scan() { + line := strings.TrimPrefix(scanner.Text(), "\ufeff") // 去掉可能的 UTF-8 BOM + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) + + idx := strings.IndexByte(line, '=') + if idx <= 0 { + continue + } + key := strings.TrimSpace(line[:idx]) + value := strings.TrimSpace(line[idx+1:]) + if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { + value = value[1 : len(value)-1] + } + if key == "" { + continue + } + if _, exists := os.LookupEnv(key); exists { + continue + } + _ = os.Setenv(key, value) + } +} + +// findDotEnvPath 依次查找:显式指定的 CANVAS_ENV_FILE、当前目录、上级目录、上上级目录。 +func findDotEnvPath() string { + if explicit := strings.TrimSpace(os.Getenv("CANVAS_ENV_FILE")); explicit != "" { + if info, err := os.Stat(explicit); err == nil && !info.IsDir() { + return explicit + } + } + candidates := []string{ + ".env", + filepath.Join("..", ".env"), + filepath.Join("..", "..", ".env"), + } + for _, candidate := range candidates { + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate + } + } + return "" +} diff --git a/backend/server b/backend/server deleted file mode 100755 index cfba15a00..000000000 Binary files a/backend/server and /dev/null differ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f488a3fa6..59e2451da 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -25,7 +25,7 @@ services: CANVAS_REGISTRATION_ENABLED: ${CANVAS_REGISTRATION_ENABLED:-false} CANVAS_ALLOW_PRIVATE_UPSTREAMS: ${CANVAS_ALLOW_PRIVATE_UPSTREAMS:-false} CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS: ${CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS:-} - CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:3000} + CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:7380} # 服务器访问地址:Agent 看图与生成参考图都靠它签发上游可取到的短时链接。 CANVAS_PUBLIC_BASE_URL: ${CANVAS_PUBLIC_BASE_URL:-} CANVAS_LOG_LEVEL: ${CANVAS_LOG_LEVEL:-debug} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index ee1a7584c..0f8aff729 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -18,7 +18,7 @@ services: CANVAS_REGISTRATION_ENABLED: ${CANVAS_REGISTRATION_ENABLED:-false} CANVAS_ALLOW_PRIVATE_UPSTREAMS: ${CANVAS_ALLOW_PRIVATE_UPSTREAMS:-false} CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS: ${CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS:-} - CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:3000} + CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:7380} # 服务器访问地址:Agent 看图与生成参考图都靠它签发上游可取到的短时链接。 CANVAS_PUBLIC_BASE_URL: ${CANVAS_PUBLIC_BASE_URL:-} ENABLE_PROVIDER_PLUGINS: ${ENABLE_PROVIDER_PLUGINS:-false} @@ -42,7 +42,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - "7380:3000" volumes: - web-data:/usr/share/nginx/html depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index e8a6ae3d8..3d728879a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: CANVAS_REGISTRATION_ENABLED: ${CANVAS_REGISTRATION_ENABLED:-false} CANVAS_ALLOW_PRIVATE_UPSTREAMS: ${CANVAS_ALLOW_PRIVATE_UPSTREAMS:-false} CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS: ${CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS:-} - CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:3000} + CANVAS_CORS_ORIGINS: ${CANVAS_CORS_ORIGINS:-http://localhost:7380} # 服务器访问地址:Agent 看图与生成参考图都靠它签发上游可取到的短时链接。 CANVAS_PUBLIC_BASE_URL: ${CANVAS_PUBLIC_BASE_URL:-} CANVAS_LOG_LEVEL: ${CANVAS_LOG_LEVEL:-info} @@ -35,7 +35,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - "7380:3000" depends_on: backend: condition: service_healthy diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index a4d894ada..fcc53d683 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -2954,6 +2954,9 @@ SSE 现在按 revision 变化发送紧凑快照,无变化只 heartbeat;客 主分支提交前复测:前端 `bun run test` 全量 1925 项通过,生产构建、类型检查、UI 退场 lint 通过;构建仍有大分块提示。同步了技能/工具标签结构与多行连接菜单的源码守护断言。后端 `go test ./...` 全量通过,工具、迁移、资源引用、handler 及工具预览权限/项目封面专项通过。本轮未执行真实模型调用、PostgreSQL 实库迁移或登录态浏览器验收,这些项目继续保留待验收。 +画布页本轮改动:区域选择(底部工具栏胶囊切到「区域选择」)下滚轮改为上下平移画布,缩放保留给 Command/Ctrl + 滚轮(含触控板捏合);带 `data-canvas-wheel-scroll` 的节点内部区域仍保持自身滚动。顶栏分享按钮右侧新增「账户与设置」按钮,点击复用画布内「去设置」的 `navigateToSettings({ section: "channels", continueCreation: true })`,进入 `/settings?section=channels&continue=1`(顶部有「返回创作 / 保存并返回」,与模型缺失时的跳转一致,不新增全局配置弹窗)。曾尝试在画布内用抽屉内嵌设置页,因嵌套 Router 报错已移除。类型检查通过;待登录态浏览器验收:区域选择的滚轮方向与实际滚动手感,设置按钮跳转后返回画布时画布内容与滚动位置是否保持,以及设置项保存后画布生成默认值立即生效。 + +节点面板快捷键:`[data-canvas-node-panel]` 面板内任意位置按 ⌘/Ctrl + Enter 触发面板主按钮(目前只有创作面板的生成按钮带 `data-canvas-panel-submit`),生成中或提示词为空时按钮禁用,快捷键不拦截按键。待验收:文本框内按下不插入换行、输入法组合中不触发、面板内其他输入框与配置面板不被误触发。 ## 画布媒体交互与 Agent 循环止损复测(2026-09-23) 本轮针对画布媒体节点、生成任务状态及云端 Agent 重复读取问题完成实现和复测。代码仍未提交,以下结论基于当前工作区的专项测试与隔离本地浏览器环境,不代表已部署生产。 diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 0eeb4e186..a61f19286 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -13,6 +13,39 @@ $dataDir = Join-Path $repoRoot ".local\project-workbench-debug" $goBuildCache = Join-Path $repoRoot ".local\cache\go-build" $goModuleCache = Join-Path $repoRoot ".local\cache\go-mod" +# 从仓库根 .env 加载配置(KEY=VALUE,支持 # 注释与空行;不存在则跳过)。 +# 后端只读取环境变量,这里把 .env 注入进程环境,子进程(go/bun)自动继承。 +$envFile = Join-Path $repoRoot ".env" +if (-not (Test-Path -LiteralPath $envFile)) { + # Windows 常见坑:在记事本等里新建文件常变成 .env.txt + $alt = Join-Path $repoRoot ".env.txt" + if (Test-Path -LiteralPath $alt) { $envFile = $alt } + else { Write-Host "未找到 $envFile,将使用内置默认值(SQLite)。如需 PostgreSQL 请先创建该文件。" -ForegroundColor Yellow } +} +if (Test-Path -LiteralPath $envFile) { + Write-Host "正在从 $envFile 加载配置..." -ForegroundColor Cyan + $loaded = @() + Get-Content -LiteralPath $envFile | ForEach-Object { + $line = $_.Trim() + if ($line -eq "" -or $line.StartsWith("#")) { return } + if ($line -match '^\s*export\s+') { $line = $line -replace '^\s*export\s+', '' } + if ($line -match '^\s*([^=]+)=(.*)$') { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + [Environment]::SetEnvironmentVariable($name, $value, [System.EnvironmentVariableTarget]::Process) + $loaded += $name + } + } + Write-Host "已加载变量:$($loaded -join ', ')" -ForegroundColor Cyan + if (-not $env:DATABASE_URL) { Write-Host "警告:DATABASE_URL 为空,后端将回退 SQLite(数据不会同步)。" -ForegroundColor Yellow } +} +# 本地绑定默认值:未显式配置时,后端只监听本机、数据落在本地调试目录 +if (-not $env:CANVAS_BACKEND_ADDR) { $env:CANVAS_BACKEND_ADDR = '127.0.0.1:7381' } +if (-not $env:CANVAS_BACKEND_DATA_DIR) { $env:CANVAS_BACKEND_DATA_DIR = $dataDir } + foreach ($commandName in @("go", "bun")) { if (-not (Get-Command $commandName -ErrorAction SilentlyContinue)) { throw "未找到 $commandName,请先安装项目要求的运行时。" @@ -45,7 +78,9 @@ function Test-ListeningPort([int]$Port) { } } -foreach ($port in @(3000, 8080)) { +# 本项目本地开发端口为前端 7380 / 后端 7381(见 AGENTS.md「本项目附加约定」)。 +# 上游默认的 3000 / 8080 在同机上被其它工程占用,不能作为探测目标。 +foreach ($port in @(7380, 7381)) { if (Test-ListeningPort $port) { throw "端口 $port 已被占用,请先关闭占用进程后重试。" } @@ -70,19 +105,17 @@ $dataDirLiteral = ConvertTo-PowerShellLiteral $dataDir $backendCommand = @" `$ErrorActionPreference = 'Stop' Set-Location -LiteralPath $backendDirLiteral -`$env:CANVAS_BACKEND_ADDR = '127.0.0.1:8080' -`$env:CANVAS_BACKEND_DATA_DIR = $dataDirLiteral `$env:GOCACHE = $(ConvertTo-PowerShellLiteral $goBuildCache) `$env:GOMODCACHE = $(ConvertTo-PowerShellLiteral $goModuleCache) -Write-Host '影策后端:http://127.0.0.1:8080' -ForegroundColor Cyan +Write-Host "影策后端:$($env:CANVAS_BACKEND_ADDR)" -ForegroundColor Cyan go run ./cmd/server "@ $webCommand = @" `$ErrorActionPreference = 'Stop' Set-Location -LiteralPath $webDirLiteral -`$env:VITE_API_PROXY_TARGET = 'http://127.0.0.1:8080' -Write-Host '影策前端:http://localhost:3000' -ForegroundColor Cyan +`$env:VITE_API_PROXY_TARGET = 'http://127.0.0.1:7381' +Write-Host '影策前端:http://localhost:7380' -ForegroundColor Cyan bun run dev "@ @@ -91,4 +124,4 @@ $webProcess = Start-Process -FilePath $powerShellPath -WindowStyle Normal -Worki Write-Host "已打开前后端开发窗口。" -ForegroundColor Green Write-Host "后端窗口 PID: $($backendProcess.Id);前端窗口 PID: $($webProcess.Id)" -Write-Host "访问 http://localhost:3000;分别在两个窗口按 Ctrl+C 停止服务。" +Write-Host "访问 http://localhost:7380;分别在两个窗口按 Ctrl+C 停止服务。" diff --git a/web/package.json b/web/package.json index cf3948d79..1d7e76c15 100644 --- a/web/package.json +++ b/web/package.json @@ -4,14 +4,14 @@ "private": true, "type": "module", "scripts": { - "dev": "vite --host 0.0.0.0 --port 3000", + "dev": "vite --host 0.0.0.0 --port 7380", "build": "tsc --noEmit && vite build", "pretest": "bun test test/tiptap-dependency-versions.test.ts test/admin-ui-regressions.test.ts test/announcement-prompt-regressions.test.ts test/password-reset.test.ts test/oss-settings.test.ts test/channel-model-editor-form.test.ts test/channel-model-price-tier-form.test.ts test/admin-logical-models.test.ts && bun test test/canvas-grid-viewport.test.tsx", "typecheck": "tsc --noEmit", "test:canvas": "bun test test/canvas-media-performance.test.ts test/canvas-node-title-interaction.test.ts test/canvas-spatial-index.test.ts", "test": "bun test", "test:previs:e2e": "bun scripts/previs-p0-chrome-e2e.mjs", - "start": "vite preview --host 0.0.0.0 --port 3000", + "start": "vite preview --host 0.0.0.0 --port 7380", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint src test" diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index 23e4155e3..52d157c6a 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -396,6 +396,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan