From dcf8fd1ff9be5f3bf997a6ed362f8369d721d770 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 28 Jun 2026 08:20:43 +0800 Subject: [PATCH] chore: consolidate admin cleanup and colima config --- .env.colima.example | 3 + .env.example | 14 +++- .gitignore | 3 + docs/DEPLOY.md | 10 +-- scripts/gadm-lb.Caddyfile | 2 +- scripts/gadm-nginx-105.conf | 10 +-- scripts/gadm-nginx.conf.example | 4 +- scripts/local_restart.sh | 17 +++-- scripts/local_restart_colima.sh | 15 ++++ scripts/release-prod.sh | 19 ++--- scripts/remote_restart.sh | 15 ++-- scripts/vite-preview.config.mjs | 2 +- server/skills/executors/goosed.mjs | 88 ++++++++++++++------- server/skills/goosed-client.mjs | 52 ++++++++----- server/skills/schema.mjs | 2 +- src/admin/pages/SkillsPage.tsx | 103 ++++++++++++++++++++++++- src/api/client.ts | 33 ++++++++ src/index.css | 119 +++++++++++++++++++++++++++++ vite.config.ts | 4 +- 19 files changed, 426 insertions(+), 89 deletions(-) create mode 100644 .env.colima.example create mode 100755 scripts/local_restart_colima.sh diff --git a/.env.colima.example b/.env.colima.example new file mode 100644 index 0000000..2467cb5 --- /dev/null +++ b/.env.colima.example @@ -0,0 +1,3 @@ +TKMIND_API_TARGET=https://127.0.0.1:18006 +MEMIND_LIB_ROOT=/Users/john/Project/Memind +VITE_MAIN_APP_URL=http://127.0.0.1:5173 diff --git a/.env.example b/.env.example index f6aa8d2..d0092ed 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ DATABASE_URL=mysql://boot:password@host:3306/goose # MYSQL_DATABASE=goose # 用户工作区根目录(创建用户时使用) -H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users +H5_USERS_ROOT=/Users/john/Project/Memind/users # 管理员(可选;设置后启动时同步到数据库) # H5_ADMIN_USERNAME=admin @@ -25,6 +25,11 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users # LLM 配置同步(可选) # TKMIND_API_TARGET=https://127.0.0.1:18006 # TKMIND_SERVER__SECRET_KEY=local-dev-secret +NODE_BIN=/opt/homebrew/opt/node@24/bin/node +AIDER_BIN=/Users/john/Project/aider/aider.sh +GOOSE_AIDER_BIN=/Users/john/Project/aider/aider.sh +OPENHANDS_BIN=/Users/john/.openhands/bin/studio-openhands +GOOSE_OPENHANDS_BIN=/Users/john/.openhands/bin/studio-openhands # ── Skill 系统 ── # goosed 节点地址(逗号分隔支持多节点;本地单节点只填一个) @@ -36,6 +41,11 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users # GOOSE_SERVER__SECRET_KEY=local-dev-secret # skill 执行超时(毫秒,默认 120000) # SKILL_GOOSED_TIMEOUT_MS=120000 +# goosed skill 会话工作目录(默认 /tmp) +# GOOSE_SKILL_WORKING_DIR=/tmp +# goosed LLM provider(与 goosed 本地 config.yaml 中的 active_provider 对应) +# GOOSE_PROVIDER=custom_deepseek +# GOOSE_MODEL=deepseek-reasoner # 套餐同步到生产后台(可选;配置后套餐增删改会自动镜像到目标后台) # PLAN_SYNC_TARGET_BASE_URL=https://md.tkmind.cn @@ -44,7 +54,7 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users # PLAN_SYNC_TIMEOUT_MS=10000 # Memind 业务模块路径(user-auth / llm-providers 等,默认 ../Memind) -# MEMIND_LIB_ROOT=/Users/john/Project/Memind +MEMIND_LIB_ROOT=/Users/john/Project/Memind # 超管页「返回对话」跳转主 H5(可选) VITE_MAIN_APP_URL=https://h5.tkmind.cn diff --git a/.gitignore b/.gitignore index 5608d60..06ec2e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .env .env.local +.env.production *.local node_modules/ dist/ @@ -9,3 +10,5 @@ memind-lib/ .vite/ *.log logs/ +.mindops/ +.adm-*.pid diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 61b6b58..4b539b0 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -59,13 +59,13 @@ ## gadm 独立登录 -`https://gadm.tkmind.cn` 应部署 **本仓库 memind_adm**,勿再反代到 `Memind/ops` Vite dev(旧版未登录会跳 `localhost:5173`)。 +`https://md.tkmind.cn` 应部署 **本仓库 memind_adm**,勿再反代到 `Memind/ops` Vite dev(旧版未登录会跳 `localhost:5173`)。 | 项 | 说明 | |---|---| | 登录 | 本页 `/auth/login`,账号来自共用 MySQL | -| 会话 | 设置 `H5_PUBLIC_BASE_URL=https://gadm.tkmind.cn`,**不要**设 `H5_COOKIE_DOMAIN=.tkmind.cn` | -| 路径 | `https://gadm.tkmind.cn/` 直达超管后台;Plaza 运营在 `/ops`;`/ops/admin` 已废弃 | +| 会话 | 设置 `H5_PUBLIC_BASE_URL=https://md.tkmind.cn`,**不要**设 `H5_COOKIE_DOMAIN=.tkmind.cn` | +| 路径 | `https://md.tkmind.cn/` 直达超管后台;Plaza 运营在 `/ops`;`/ops/admin` 已废弃 | | API | `ADM_DEV_BACKEND=http://127.0.0.1:8085`(preview 反代目标) | | 初始化 admin | `npm run admin:init` 只更新 `Memind` 主用户表中的 admin 密码,不创建独立后台账号 | @@ -187,7 +187,7 @@ ssh john@10.10.0.2 'tail -f /Users/john/Project/memind_adm/adm-preview.log' # preview 反代目标(/auth、/admin-api、/api)— 指向本仓库 Admin API ADM_API_PORT=8085 ADM_DEV_BACKEND=http://127.0.0.1:8085 -H5_PUBLIC_BASE_URL=https://gadm.tkmind.cn +H5_PUBLIC_BASE_URL=https://md.tkmind.cn VITE_BASE_PATH=/ops # 共享 Memind 主库与主实现 @@ -196,7 +196,7 @@ MEMIND_LIB_ROOT=/Users/john/Project/Memind H5_USERS_ROOT=/Users/john/Project/Memind/users # 套餐自动同步到生产后台(可选) -PLAN_SYNC_TARGET_BASE_URL=https://gadm.tkmind.cn +PLAN_SYNC_TARGET_BASE_URL=https://md.tkmind.cn PLAN_SYNC_USERNAME=admin PLAN_SYNC_PASSWORD= PLAN_SYNC_TIMEOUT_MS=10000 diff --git a/scripts/gadm-lb.Caddyfile b/scripts/gadm-lb.Caddyfile index 182ada6..c6be6df 100644 --- a/scripts/gadm-lb.Caddyfile +++ b/scripts/gadm-lb.Caddyfile @@ -1,4 +1,4 @@ -# gadm.tkmind.cn → memind_adm(vite preview :5174 + Admin API :8085) +# md.tkmind.cn → memind_adm(vite preview :5174 + Admin API :8085) # 生产 nginx 需把 /ops/ 反代到 5174,勿再指向 Memind/ops dev server :8091 { reverse_proxy 127.0.0.1:5174 { diff --git a/scripts/gadm-nginx-105.conf b/scripts/gadm-nginx-105.conf index 7bc5bbe..7de9cec 100644 --- a/scripts/gadm-nginx-105.conf +++ b/scripts/gadm-nginx-105.conf @@ -1,10 +1,10 @@ -# gadm.tkmind.cn @ 105 nginx → Studio LAN 10.10.0.2(fallback 58.38.22.103) +# md.tkmind.cn @ 105 nginx → Studio LAN 10.10.0.2(fallback 58.38.22.103) # memind_adm: preview :5174, Admin API :8085(根路径部署,/ 直达后台) server { listen 80; listen [::]:80; - server_name gadm.tkmind.cn; + server_name md.tkmind.cn; location /.well-known/acme-challenge/ { root /var/www/certbot; @@ -18,9 +18,9 @@ server { server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name gadm.tkmind.cn; - ssl_certificate /etc/letsencrypt/live/gadm.tkmind.cn/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/gadm.tkmind.cn/privkey.pem; + server_name md.tkmind.cn; + ssl_certificate /etc/letsencrypt/live/md.tkmind.cn/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/md.tkmind.cn/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/scripts/gadm-nginx.conf.example b/scripts/gadm-nginx.conf.example index 2fd5bc2..f4edfae 100644 --- a/scripts/gadm-nginx.conf.example +++ b/scripts/gadm-nginx.conf.example @@ -1,4 +1,4 @@ -# gadm.tkmind.cn — memind_adm 独立管理端(替换原 Memind /ops Vite dev) +# md.tkmind.cn — memind_adm 独立管理端(替换原 Memind /ops Vite dev) # # 前置:100 服务器 remote_restart.sh 已启动 # Admin API: 127.0.0.1:8085 @@ -8,7 +8,7 @@ server { listen 443 ssl http2; - server_name gadm.tkmind.cn; + server_name md.tkmind.cn; # ssl_certificate ...; diff --git a/scripts/local_restart.sh b/scripts/local_restart.sh index 1085fb5..86494ff 100755 --- a/scripts/local_restart.sh +++ b/scripts/local_restart.sh @@ -4,24 +4,27 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" PORT="${ADM_PORT:-5174}" API_PORT="${ADM_API_PORT:-8085}" -NODE_PATH="${LOCAL_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" LOG="${ROOT}/adm-preview.log" API_LOG="${ROOT}/adm-api.log" PID_FILE="${ROOT}/.adm-preview.pid" API_PID_FILE="${ROOT}/.adm-api.pid" -export PATH="${NODE_PATH}:${PATH}" -command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; } +[[ -x "${NODE_BIN}" ]] || { echo "❌ NODE_BIN 不可执行: ${NODE_BIN}" >&2; exit 1; } +NODE_DIR="$(dirname "${NODE_BIN}")" +export PATH="${NODE_DIR}:${PATH}" +NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}" +[[ -x "${NPM_BIN}" ]] || { echo "❌ NPM_BIN 不可执行: ${NPM_BIN}" >&2; exit 1; } cd "${ROOT}" if [[ ! -d node_modules ]]; then echo "==> 安装依赖..." - npm install + "${NPM_BIN}" install fi echo "==> 本机构建..." -npm run build +"${NPM_BIN}" run build echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..." lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true @@ -32,7 +35,7 @@ lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null rm -f "${PID_FILE}" "${API_PID_FILE}" echo "==> 启动本机 Admin API @ 127.0.0.1:${API_PORT}..." -nohup node server/index.mjs >>"${API_LOG}" 2>&1 & +nohup "${NODE_BIN}" server/index.mjs >>"${API_LOG}" 2>&1 & echo $! >"${API_PID_FILE}" sleep 2 @@ -43,7 +46,7 @@ if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then fi echo "==> 启动本机 vite preview @ 127.0.0.1:${PORT}..." -nohup npm run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 & +nohup "${NPM_BIN}" run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 & echo $! >"${PID_FILE}" sleep 2 diff --git a/scripts/local_restart_colima.sh b/scripts/local_restart_colima.sh new file mode 100755 index 0000000..f8bfcb6 --- /dev/null +++ b/scripts/local_restart_colima.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +cd "${ROOT}" + +set -a +source "${ROOT}/.env" +if [[ -f "${ROOT}/.env.colima" ]]; then + source "${ROOT}/.env.colima" +fi +set +a + +exec bash "${ROOT}/scripts/local_restart.sh" diff --git a/scripts/release-prod.sh b/scripts/release-prod.sh index 64fb65a..b5882a0 100755 --- a/scripts/release-prod.sh +++ b/scripts/release-prod.sh @@ -116,6 +116,10 @@ say "打包 Memind 共享模块到 memind-lib" rm -rf "${MEMIND_LIB}" mkdir -p "${MEMIND_LIB}" find "${MEMIND_SRC}" -maxdepth 1 -name '*.mjs' -exec cp -f {} "${MEMIND_LIB}/" \; +# 同步技能目录(skills/ 下的 SKILL.md 是技能目录来源) +if [[ -d "${MEMIND_SRC}/skills" ]]; then + cp -a "${MEMIND_SRC}/skills" "${MEMIND_LIB}/" +fi [[ -f "${MEMIND_LIB}/user-auth.mjs" ]] || { echo "memind-lib 缺少 user-auth.mjs" >&2 exit 1 @@ -225,15 +229,12 @@ done say "安装运行依赖" cd "${RELEASE_DIR}" -export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin:${PATH}" -if command -v npm >/dev/null 2>&1; then - NPM_BIN="$(command -v npm)" -elif [[ -x /opt/homebrew/opt/node@24/bin/npm ]]; then - NPM_BIN="/opt/homebrew/opt/node@24/bin/npm" -else - echo "npm not found on remote" >&2 - exit 1 -fi +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +[[ -x "${NODE_BIN}" ]] || { echo "NODE_BIN is not executable: ${NODE_BIN}" >&2; exit 1; } +NODE_DIR="$(dirname "${NODE_BIN}")" +export PATH="${NODE_DIR}:${PATH}" +NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}" +[[ -x "${NPM_BIN}" ]] || { echo "NPM_BIN is not executable: ${NPM_BIN}" >&2; exit 1; } if [[ -f package-lock.json ]]; then "${NPM_BIN}" ci --no-audit --no-fund else diff --git a/scripts/remote_restart.sh b/scripts/remote_restart.sh index b7f6240..db20fb7 100755 --- a/scripts/remote_restart.sh +++ b/scripts/remote_restart.sh @@ -4,15 +4,18 @@ set -euo pipefail ROOT="${ADM_ROOT:-/Users/john/Project/memind_adm}" PORT="${ADM_PORT:-5174}" -NODE_PATH="${LAN_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" API_PORT="${ADM_API_PORT:-8085}" LOG="${ROOT}/adm-preview.log" API_LOG="${ROOT}/adm-api.log" PID_FILE="${ROOT}/.adm-preview.pid" API_PID_FILE="${ROOT}/.adm-api.pid" -export PATH="${NODE_PATH}:${PATH}" -command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; } +[[ -x "${NODE_BIN}" ]] || { echo "❌ NODE_BIN 不可执行: ${NODE_BIN}" >&2; exit 1; } +NODE_DIR="$(dirname "${NODE_BIN}")" +export PATH="${NODE_DIR}:${PATH}" +NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}" +[[ -x "${NPM_BIN}" ]] || { echo "❌ NPM_BIN 不可执行: ${NPM_BIN}" >&2; exit 1; } cd "${ROOT}" @@ -41,7 +44,7 @@ fi if [[ ! -d node_modules ]]; then echo "==> 安装依赖..." - npm install + "${NPM_BIN}" install fi echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..." @@ -59,7 +62,7 @@ lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null sleep 1 echo "==> 启动 Admin API @ ${API_HOST}:${API_PORT}..." -nohup node server/index.mjs >>"${API_LOG}" 2>&1 & +nohup "${NODE_BIN}" server/index.mjs >>"${API_LOG}" 2>&1 & echo $! >"${API_PID_FILE}" sleep 2 @@ -70,7 +73,7 @@ if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then fi echo "==> 启动 vite preview @ ${WEB_HOST}:${PORT} (base=${BASE_PATH})..." -nohup npx vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 & +nohup "${NPM_BIN}" exec vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 & echo $! >"${PID_FILE}" sleep 3 diff --git a/scripts/vite-preview.config.mjs b/scripts/vite-preview.config.mjs index d4c8084..36d84d7 100644 --- a/scripts/vite-preview.config.mjs +++ b/scripts/vite-preview.config.mjs @@ -12,7 +12,7 @@ export default defineConfig(({ mode }) => { host: env.ADM_WEB_HOST ?? env.ADM_API_HOST ?? '127.0.0.1', port: Number(env.ADM_PORT ?? 5174), strictPort: true, - allowedHosts: ['gadm.tkmind.cn', 'localhost', '127.0.0.1', '10.10.0.2', '58.38.22.103'], + allowedHosts: ['md.tkmind.cn', 'gadm.tkmind.cn', 'localhost', '127.0.0.1', '10.10.0.2', '58.38.22.103'], proxy: { '/api': backend, '/auth': backend, diff --git a/server/skills/executors/goosed.mjs b/server/skills/executors/goosed.mjs index 66b0a1d..311a3f8 100644 --- a/server/skills/executors/goosed.mjs +++ b/server/skills/executors/goosed.mjs @@ -1,16 +1,18 @@ /** - * GoosedExecutor — runs a skill via a one-shot goosed session. + * GoosedExecutor — runs a skill via a goosed session. * * Session lifecycle: - * 1. POST /agent/start { messages: [{ role: 'user', content: prompt }] } - * 2. GET /sessions/:id/events (SSE) → collect until Finish event or timeout - * 3. DELETE /sessions/:id (best-effort cleanup) + * 1. POST /agent/start { working_dir } + * 2. POST /sessions/:id/reply { request_id: UUID, user_message: {...} } + * 3. GET /sessions/:id/events (SSE) → collect until Finish event or timeout + * 4. DELETE /sessions/:id (best-effort cleanup) * * Dual-node: pickGoosedUrl() round-robins across configured nodes. * Protocol: HTTPS with self-signed cert (rejectUnauthorized: false). */ import https from 'node:https'; +import crypto from 'node:crypto'; import { goosedFetch, pickGoosedUrl } from '../goosed-client.mjs'; const SESSION_TIMEOUT_MS = Number(process.env.SKILL_GOOSED_TIMEOUT_MS ?? 120_000); @@ -18,19 +20,14 @@ const tlsAgent = new https.Agent({ rejectUnauthorized: false }); export async function invokeGoosedSkill(skill, input) { const node = pickGoosedUrl(); + const prompt = buildPrompt(skill, input); - const systemPrompt = buildSystemPrompt(skill, input); - const userPrompt = buildUserPrompt(skill, input); - - // 1. Start session + // 1. Start session (no messages yet) const startRes = await goosedFetch('/agent/start', { method: 'POST', node, body: { - version: '1.0.0', - session_id: null, - ...(systemPrompt ? { system_prompt_override: systemPrompt } : {}), - messages: [{ role: 'user', content: userPrompt }], + working_dir: process.env.GOOSE_SKILL_WORKING_DIR ?? '/tmp', }, }); @@ -41,12 +38,42 @@ export async function invokeGoosedSkill(skill, input) { ); } - const sessionId = startRes.body?.session_id ?? startRes.body?.id; + const sessionId = startRes.body?.id ?? startRes.body?.session_id; if (!sessionId) { - throw new Error(`goosed did not return a session_id: ${JSON.stringify(startRes.body)}`); + throw new Error(`goosed did not return a session id: ${JSON.stringify(startRes.body)}`); } - // 2. Stream events and collect result + // 1b. Set the LLM provider on the session + const goosedProvider = process.env.GOOSE_PROVIDER ?? process.env.GOOSED_PROVIDER ?? 'custom_deepseek'; + const goosedModel = process.env.GOOSE_MODEL ?? process.env.GOOSED_MODEL; + const providerBody = { session_id: sessionId, provider: goosedProvider }; + if (goosedModel) providerBody.model = goosedModel; + await goosedFetch('/agent/update_provider', { method: 'POST', node, body: providerBody }); + + // 2. Send the prompt via reply + const requestId = crypto.randomUUID(); + const replyRes = await goosedFetch(`/sessions/${sessionId}/reply`, { + method: 'POST', + node, + body: { + request_id: requestId, + user_message: { + role: 'user', + created: Math.floor(Date.now() / 1000), + metadata: { userVisible: true, agentVisible: true }, + content: [{ type: 'text', text: prompt }], + }, + }, + }); + + if (!replyRes.ok) { + throw Object.assign( + new Error(`goosed /sessions/${sessionId}/reply failed (${replyRes.status})`), + { status: replyRes.status, upstream: replyRes.body }, + ); + } + + // 3. Stream SSE events and collect result try { return await collectSessionResult(node, sessionId); } finally { @@ -54,14 +81,22 @@ export async function invokeGoosedSkill(skill, input) { } } -function buildSystemPrompt(skill, input) { - if (skill.runtime?.config?.system_prompt) return skill.runtime.config.system_prompt; - return null; -} - -function buildUserPrompt(skill, input) { +function buildPrompt(skill, input) { if (input.design_md_content) { - return `You are a UI generation assistant. Follow this design system exactly:\n\n${input.design_md_content}\n\nTask: ${input.task ?? input.prompt ?? ''}`; + return `You are a UI generation assistant. Your output must be a complete, self-contained HTML page — not a component, not a snippet, not a code block with backticks. Output raw HTML only, starting with . + +Apply the following design system exactly. Match the colors, typography, spacing, border styles, shadows, and interaction patterns described: + +${input.design_md_content} + +Task: ${input.task ?? input.prompt ?? ''} + +Requirements: +- Output a single complete HTML file with , , and all inline